diff --git a/PlayerTags/PluginConfiguration.cs b/PlayerTags/Configuration/PluginConfiguration.cs similarity index 96% rename from PlayerTags/PluginConfiguration.cs rename to PlayerTags/Configuration/PluginConfiguration.cs index 255f62e..bfad873 100644 --- a/PlayerTags/PluginConfiguration.cs +++ b/PlayerTags/Configuration/PluginConfiguration.cs @@ -1,11 +1,13 @@ using Dalamud.Configuration; using Dalamud.Plugin; using Newtonsoft.Json; +using PlayerTags.Data; +using PlayerTags.Inheritables; using System; using System.Collections.Generic; using System.Linq; -namespace PlayerTags +namespace PlayerTags.Configuration { [Serializable] public class PluginConfiguration : IPluginConfiguration @@ -16,7 +18,7 @@ namespace PlayerTags public NameplateTitleVisibility NameplateTitleVisibility = NameplateTitleVisibility.WhenHasTags; public NameplateTitlePosition NameplateTitlePosition = NameplateTitlePosition.AlwaysAboveName; public bool IsPlayerNameRandomlyGenerated = false; - public bool IsCustomTagContextMenuEnabled = true; + public bool IsCustomTagsContextMenuEnabled = true; public bool IsShowInheritedPropertiesEnabled = true; [JsonProperty(TypeNameHandling = TypeNameHandling.None, ItemTypeNameHandling = TypeNameHandling.None)] diff --git a/PlayerTags/PluginConfigurationUI.cs b/PlayerTags/Configuration/PluginConfigurationUI.cs similarity index 96% rename from PlayerTags/PluginConfigurationUI.cs rename to PlayerTags/Configuration/PluginConfigurationUI.cs index 5b6efb9..9ada69d 100644 --- a/PlayerTags/PluginConfigurationUI.cs +++ b/PlayerTags/Configuration/PluginConfigurationUI.cs @@ -1,35 +1,29 @@ -using Dalamud.Game.ClientState; -using Dalamud.Game.ClientState.Objects; -using Dalamud.Game.ClientState.Objects.SubKinds; -using Dalamud.Game.ClientState.Party; +using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Interface; -using FFXIVClientStructs.FFXIV.Client.Game.Character; using ImGuiNET; using Lumina.Excel.GeneratedSheets; +using PlayerTags.Data; +using PlayerTags.Inheritables; +using PlayerTags.PluginStrings; using PlayerTags.Resources; using System; using System.Collections.Generic; using System.Linq; using System.Numerics; -namespace PlayerTags +namespace PlayerTags.Configuration { public class PluginConfigurationUI { private PluginConfiguration m_PluginConfiguration; private PluginData m_PluginData; - private ClientState m_ClientState; - private PartyList m_PartyList; - private ObjectTable m_ObjectTable; + private InheritableValue? m_ColorPickerPopupDataContext; - public PluginConfigurationUI(PluginConfiguration config, PluginData pluginData, ClientState clientState, PartyList partyList, ObjectTable objectTable) + public PluginConfigurationUI(PluginConfiguration config, PluginData pluginData) { m_PluginConfiguration = config; m_PluginData = pluginData; - m_ClientState = clientState; - m_PartyList = partyList; - m_ObjectTable = objectTable; } public void Draw() @@ -54,7 +48,7 @@ namespace PlayerTags ImGui.Spacing(); ImGui.Spacing(); ImGui.TreePush(); - DrawCheckbox(nameof(m_PluginConfiguration.IsCustomTagContextMenuEnabled), true, ref m_PluginConfiguration.IsCustomTagContextMenuEnabled, () => m_PluginConfiguration.Save(m_PluginData)); + DrawCheckbox(nameof(m_PluginConfiguration.IsCustomTagsContextMenuEnabled), true, ref m_PluginConfiguration.IsCustomTagsContextMenuEnabled, () => m_PluginConfiguration.Save(m_PluginData)); ImGui.TreePop(); @@ -124,15 +118,15 @@ namespace PlayerTags ImGui.TableHeadersRow(); int rowIndex = 0; - foreach (var partyMember in m_PartyList.OrderBy(obj => obj.Name.TextValue).ToArray()) + foreach (var partyMember in PluginServices.PartyList.OrderBy(obj => obj.Name.TextValue).ToArray()) { DrawPlayerAssignmentRow(partyMember.Name.TextValue, rowIndex); ++rowIndex; } - if (m_PartyList.Length == 0 && m_ClientState.LocalPlayer != null) + if (PluginServices.PartyList.Length == 0 && PluginServices.ClientState.LocalPlayer != null) { - DrawPlayerAssignmentRow(m_ClientState.LocalPlayer.Name.TextValue, 0); + DrawPlayerAssignmentRow(PluginServices.ClientState.LocalPlayer.Name.TextValue, 0); } ImGui.EndTable(); @@ -161,15 +155,15 @@ namespace PlayerTags ImGui.TableHeadersRow(); int rowIndex = 0; - foreach (var gameObject in m_ObjectTable.Where(obj => obj is PlayerCharacter).OrderBy(obj => obj.Name.TextValue)) + foreach (var gameObject in PluginServices.ObjectTable.Where(obj => obj is PlayerCharacter).OrderBy(obj => obj.Name.TextValue)) { DrawPlayerAssignmentRow(gameObject.Name.TextValue, rowIndex); ++rowIndex; } - if (m_ObjectTable.Length == 0 && m_ClientState.LocalPlayer != null) + if (PluginServices.ObjectTable.Length == 0 && PluginServices.ClientState.LocalPlayer != null) { - DrawPlayerAssignmentRow(m_ClientState.LocalPlayer.Name.TextValue, 0); + DrawPlayerAssignmentRow(PluginServices.ClientState.LocalPlayer.Name.TextValue, 0); } ImGui.EndTable(); @@ -204,9 +198,9 @@ namespace PlayerTags ++rowIndex; } - if (m_ObjectTable.Length == 0 && m_ClientState.LocalPlayer != null) + if (PluginServices.ObjectTable.Length == 0 && PluginServices.ClientState.LocalPlayer != null) { - DrawPlayerAssignmentRow(m_ClientState.LocalPlayer.Name.TextValue, 0); + DrawPlayerAssignmentRow(PluginServices.ClientState.LocalPlayer.Name.TextValue, 0); } ImGui.EndTable(); diff --git a/PlayerTags/DefaultPluginData.cs b/PlayerTags/Data/DefaultPluginData.cs similarity index 80% rename from PlayerTags/DefaultPluginData.cs rename to PlayerTags/Data/DefaultPluginData.cs index 576e404..b6f5357 100644 --- a/PlayerTags/DefaultPluginData.cs +++ b/PlayerTags/Data/DefaultPluginData.cs @@ -1,32 +1,37 @@ using Dalamud.Data; using Dalamud.Game.Text.SeStringHandling; using Lumina.Excel.GeneratedSheets; +using PlayerTags.Inheritables; +using PlayerTags.PluginStrings; using System.Collections.Generic; using System.Linq; -namespace PlayerTags +namespace PlayerTags.Data { public class DefaultPluginData { - public Dictionary RolesById { get; } = new Dictionary() + public Dictionary RolesById { get; } + public Dictionary RolesByJobAbbreviation { get; } + + public Dictionary AllTagsChanges { get; } + public Dictionary AllRoleTagsChanges { get; } + public Dictionary> RoleTagsChanges { get; } + public Dictionary> JobTagsChanges { get; } + public Dictionary AllCustomTagsChanges { get; } + + public DefaultPluginData() { - { 0, Role.LandHand }, - { 1, Role.Tank }, - { 2, Role.DPS }, - { 3, Role.DPS }, - { 4, Role.Healer }, - }; + RolesById = new Dictionary() + { + { 0, Role.LandHand }, + { 1, Role.Tank }, + { 2, Role.DPS }, + { 3, Role.DPS }, + { 4, Role.Healer }, + }; - public Dictionary RolesByJobAbbreviation { get; } = new Dictionary(); + RolesByJobAbbreviation = new Dictionary(); - public Dictionary AllTagsChanges = new Dictionary(); - public Dictionary AllRoleTagsChanges = new Dictionary(); - public Dictionary> RoleTagsChanges = new Dictionary>(); - public Dictionary> JobTagsChanges = new Dictionary>(); - public Dictionary AllCustomTagsChanges = new Dictionary(); - - public void Initialize(DataManager dataManager) - { AllTagsChanges = new Tag(new LiteralPluginString("")) { IsSelected = true, @@ -45,6 +50,7 @@ namespace PlayerTags IsTextVisibleInNameplates = true, }.GetChanges(); + RoleTagsChanges = new Dictionary>(); RoleTagsChanges[Role.LandHand] = new Tag(new LiteralPluginString("")) { IsSelected = false, @@ -77,9 +83,10 @@ namespace PlayerTags TextColor = 508, }.GetChanges(); + JobTagsChanges = new Dictionary>(); foreach ((var role, var roleTagChanges) in RoleTagsChanges) { - var classJobs = dataManager.GetExcelSheet(); + var classJobs = PluginServices.DataManager.GetExcelSheet(); if (classJobs == null) { break; diff --git a/PlayerTags/NameplateElement.cs b/PlayerTags/Data/NameplateElement.cs similarity index 77% rename from PlayerTags/NameplateElement.cs rename to PlayerTags/Data/NameplateElement.cs index 3636539..8a10b25 100644 --- a/PlayerTags/NameplateElement.cs +++ b/PlayerTags/Data/NameplateElement.cs @@ -1,4 +1,4 @@ -namespace PlayerTags +namespace PlayerTags.Data { public enum NameplateElement { diff --git a/PlayerTags/NameplateFreeCompanyVisibility.cs b/PlayerTags/Data/NameplateFreeCompanyVisibility.cs similarity index 76% rename from PlayerTags/NameplateFreeCompanyVisibility.cs rename to PlayerTags/Data/NameplateFreeCompanyVisibility.cs index 9a37007..9f00b2d 100644 --- a/PlayerTags/NameplateFreeCompanyVisibility.cs +++ b/PlayerTags/Data/NameplateFreeCompanyVisibility.cs @@ -1,4 +1,4 @@ -namespace PlayerTags +namespace PlayerTags.Data { public enum NameplateFreeCompanyVisibility { diff --git a/PlayerTags/NameplateTitlePosition.cs b/PlayerTags/Data/NameplateTitlePosition.cs similarity index 80% rename from PlayerTags/NameplateTitlePosition.cs rename to PlayerTags/Data/NameplateTitlePosition.cs index 93f30bc..7c8613e 100644 --- a/PlayerTags/NameplateTitlePosition.cs +++ b/PlayerTags/Data/NameplateTitlePosition.cs @@ -1,4 +1,4 @@ -namespace PlayerTags +namespace PlayerTags.Data { public enum NameplateTitlePosition { diff --git a/PlayerTags/NameplateTitleVisibility.cs b/PlayerTags/Data/NameplateTitleVisibility.cs similarity index 81% rename from PlayerTags/NameplateTitleVisibility.cs rename to PlayerTags/Data/NameplateTitleVisibility.cs index fa3368d..0cba968 100644 --- a/PlayerTags/NameplateTitleVisibility.cs +++ b/PlayerTags/Data/NameplateTitleVisibility.cs @@ -1,4 +1,4 @@ -namespace PlayerTags +namespace PlayerTags.Data { public enum NameplateTitleVisibility { diff --git a/PlayerTags/PluginData.cs b/PlayerTags/Data/PluginData.cs similarity index 92% rename from PlayerTags/PluginData.cs rename to PlayerTags/Data/PluginData.cs index 5633363..f5b12bb 100644 --- a/PlayerTags/PluginData.cs +++ b/PlayerTags/Data/PluginData.cs @@ -1,8 +1,9 @@ -using Dalamud.Data; +using PlayerTags.Configuration; +using PlayerTags.PluginStrings; using System; using System.Collections.Generic; -namespace PlayerTags +namespace PlayerTags.Data { public class PluginData { @@ -14,28 +15,20 @@ namespace PlayerTags public Tag AllCustomTags; public List CustomTags; - public PluginData() + public PluginData(PluginConfiguration pluginConfiguration) { Default = new DefaultPluginData(); - AllTags = new Tag(new LocalizedPluginString(nameof(AllTags))); - AllRoleTags = new Tag(new LocalizedPluginString(nameof(AllRoleTags))); - RoleTags = new Dictionary(); - JobTags = new Dictionary(); - AllCustomTags = new Tag(new LocalizedPluginString(nameof(AllCustomTags))); - CustomTags = new List(); - } - - public void Initialize(DataManager dataManager, PluginConfiguration pluginConfiguration) - { - Default.Initialize(dataManager); // Set the default changes and saved changes + AllTags = new Tag(new LocalizedPluginString(nameof(AllTags))); AllTags.SetChanges(Default.AllTagsChanges); AllTags.SetChanges(pluginConfiguration.AllTagsChanges); + AllRoleTags = new Tag(new LocalizedPluginString(nameof(AllRoleTags))); AllRoleTags.SetChanges(Default.AllRoleTagsChanges); AllRoleTags.SetChanges(pluginConfiguration.AllRoleTagsChanges); + RoleTags = new Dictionary(); foreach (var role in Enum.GetValues()) { RoleTags[role] = new Tag(new LocalizedPluginString(Localizer.GetName(role))); @@ -67,6 +60,9 @@ namespace PlayerTags } } + AllCustomTags = new Tag(new LocalizedPluginString(nameof(AllCustomTags))); + CustomTags = new List(); + AllCustomTags.SetChanges(Default.AllCustomTagsChanges); AllCustomTags.SetChanges(pluginConfiguration.AllCustomTagsChanges); diff --git a/PlayerTags/Role.cs b/PlayerTags/Data/Role.cs similarity index 76% rename from PlayerTags/Role.cs rename to PlayerTags/Data/Role.cs index e1dc976..69a4780 100644 --- a/PlayerTags/Role.cs +++ b/PlayerTags/Data/Role.cs @@ -1,4 +1,4 @@ -namespace PlayerTags +namespace PlayerTags.Data { public enum Role { diff --git a/PlayerTags/Tag.cs b/PlayerTags/Data/Tag.cs similarity index 98% rename from PlayerTags/Tag.cs rename to PlayerTags/Data/Tag.cs index e762643..6ca34ee 100644 --- a/PlayerTags/Tag.cs +++ b/PlayerTags/Data/Tag.cs @@ -1,9 +1,11 @@ using Dalamud.Game.Text.SeStringHandling; +using PlayerTags.Inheritables; +using PlayerTags.PluginStrings; using System; using System.Collections.Generic; using System.Linq; -namespace PlayerTags +namespace PlayerTags.Data { public class Tag { diff --git a/PlayerTags/TagPosition.cs b/PlayerTags/Data/TagPosition.cs similarity index 75% rename from PlayerTags/TagPosition.cs rename to PlayerTags/Data/TagPosition.cs index 44ae941..4dc027f 100644 --- a/PlayerTags/TagPosition.cs +++ b/PlayerTags/Data/TagPosition.cs @@ -1,4 +1,4 @@ -namespace PlayerTags +namespace PlayerTags.Data { public enum TagPosition { diff --git a/PlayerTags/TagTarget.cs b/PlayerTags/Data/TagTarget.cs similarity index 71% rename from PlayerTags/TagTarget.cs rename to PlayerTags/Data/TagTarget.cs index 42a5429..8cb97af 100644 --- a/PlayerTags/TagTarget.cs +++ b/PlayerTags/Data/TagTarget.cs @@ -1,4 +1,4 @@ -namespace PlayerTags +namespace PlayerTags.Data { public enum TagTarget { diff --git a/PlayerTags/Features/ChatTagTargetFeature.cs b/PlayerTags/Features/ChatTagTargetFeature.cs new file mode 100644 index 0000000..49fc305 --- /dev/null +++ b/PlayerTags/Features/ChatTagTargetFeature.cs @@ -0,0 +1,230 @@ +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.Collections.Generic; +using System.Linq; + +namespace PlayerTags.Features +{ + public class ChatTagTargetFeature : TagTargetFeature + { + private PluginConfiguration m_PluginConfiguration; + private PluginData m_PluginData; + + public ChatTagTargetFeature(PluginConfiguration pluginConfiguration, PluginData pluginData) + : base(pluginConfiguration) + { + m_PluginConfiguration = pluginConfiguration; + m_PluginData = pluginData; + + PluginServices.ChatGui.ChatMessage += Chat_ChatMessage; + } + + public override void Dispose() + { + PluginServices.ChatGui.ChatMessage -= Chat_ChatMessage; + base.Dispose(); + } + + private void Chat_ChatMessage(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled) + { + AddTagsToChat(sender, out _); + AddTagsToChat(message, out _); + } + + protected override BitmapFontIcon? GetIcon(Tag tag) + { + if (tag.IsIconVisibleInChat.InheritedValue != null && tag.IsIconVisibleInChat.InheritedValue.Value) + { + return tag.Icon.InheritedValue; + } + + return null; + } + + protected override string? GetText(Tag tag) + { + if (tag.IsTextVisibleInChat.InheritedValue != null && tag.IsTextVisibleInChat.InheritedValue.Value) + { + return tag.Text.InheritedValue; + } + + return null; + } + + /// + /// A match found within a string. + /// + private class StringMatch + { + /// + /// The string that the match was found in. + /// + public SeString SeString { get; init; } + + /// + /// The matching text payload. + /// + public TextPayload TextPayload { get; init; } + + /// + /// The matching game object if one exists + /// + public GameObject? GameObject { get; init; } + + /// + /// A matching player payload if one exists. + /// + public PlayerPayload? PlayerPayload { get; init; } + + public StringMatch(SeString seString, TextPayload textPayload) + { + SeString = seString; + TextPayload = textPayload; + } + + /// + /// Gets the matches text. + /// + /// The match text. + public string GetMatchText() + { + if (GameObject != null) + { + return GameObject.Name.TextValue; + } + + return TextPayload.Text; + } + } + + /// + /// Searches the given string for game object matches. + /// + /// The string to search. + /// A list of matched game objects. + private List GetStringMatches(SeString seString) + { + List stringMatches = new List(); + + for (int payloadIndex = 0; payloadIndex < seString.Payloads.Count; ++payloadIndex) + { + var payload = seString.Payloads[payloadIndex]; + if (payload is PlayerPayload playerPayload) + { + var gameObject = PluginServices.ObjectTable.FirstOrDefault(gameObject => gameObject.Name.TextValue == playerPayload.PlayerName); + + // The next payload MUST be a text payload + if (payloadIndex + 1 < seString.Payloads.Count && seString.Payloads[payloadIndex + 1] is TextPayload textPayload) + { + var stringMatch = new StringMatch(seString, textPayload) + { + GameObject = gameObject, + PlayerPayload = playerPayload + }; + stringMatches.Add(stringMatch); + + // Don't handle the text payload twice + payloadIndex++; + } + else + { + PluginLog.Error("Expected payload after player payload to be a text payload but it wasn't"); + } + } + + /// TODO: Not sure if this is desirable. Enabling this allows tags to appear next to the name of the local player by text in chat because the local player doesn't have a player payload. + /// But because it's just a simple string comparison, it won't work in all circumstances. E.g. in party chat the player name is wrapped in (). To be comprehensive we need to search substring. + /// This means we would need to think about breaking down existing payloads to split them out. + /// If we decide to do that, we could even for example find unlinked player names in chat and add player payloads for them. + // If it's just a text payload then either a character NEEDS to exist for it, or it needs to be identified as a character by custom tag configs + //else if (payload is TextPayload textPayload) + //{ + // var gameObject = ObjectTable.FirstOrDefault(gameObject => gameObject.Name.TextValue == textPayload.Text); + // var isIncludedInCustomTagConfig = m_Config.CustomTags.Any(customTagConfig => customTagConfig.IncludesGameObjectName(textPayload.Text)); + + // if (gameObject != null || isIncludedInCustomTagConfig) + // { + // var stringMatch = new StringMatch(seString, textPayload) + // { + // GameObject = gameObject + // }; + // stringMatches.Add(stringMatch); + // } + //} + } + + return stringMatches; + } + + /// + /// Adds all configured tags to chat. + /// + /// The message to change. + /// Whether the message was changed. + private void AddTagsToChat(SeString message, out bool isMessageChanged) + { + isMessageChanged = false; + + var stringMatches = GetStringMatches(message); + foreach (var stringMatch in stringMatches) + { + Dictionary> stringChanges = new Dictionary>(); + + // The role tag payloads + if (stringMatch.GameObject is Character character) + { + // Add the job tag + if (m_PluginData.JobTags.TryGetValue(character.ClassJob.GameData.Abbreviation, out var jobTag)) + { + if (jobTag.TagPositionInChat.InheritedValue != null) + { + var payloads = GetPayloads(jobTag); + if (payloads.Any()) + { + AddPayloadChanges(jobTag.TagPositionInChat.InheritedValue.Value, payloads, stringChanges); + } + } + } + + // Add randomly generated name tag payload + if (m_PluginConfiguration.IsPlayerNameRandomlyGenerated) + { + var playerName = stringMatch.GetMatchText(); + if (playerName != null) + { + var generatedName = RandomNameGenerator.Generate(playerName); + if (generatedName != null) + { + AddPayloadChanges(TagPosition.Replace, Enumerable.Empty().Append(new TextPayload(generatedName)), stringChanges); + } + } + } + } + + // Add the custom tag payloads + foreach (var customTag in m_PluginData.CustomTags) + { + if (customTag.TagPositionInChat.InheritedValue != null) + { + if (customTag.IncludesGameObjectNameToApplyTo(stringMatch.GetMatchText())) + { + var customTagPayloads = GetPayloads(customTag); + if (customTagPayloads.Any()) + { + AddPayloadChanges(customTag.TagPositionInChat.InheritedValue.Value, customTagPayloads, stringChanges); + } + } + } + } + + ApplyStringChanges(message, stringChanges, stringMatch.TextPayload); + isMessageChanged = true; + } + } + } +} diff --git a/PlayerTags/Features/CustomTagsContextMenuFeature.cs b/PlayerTags/Features/CustomTagsContextMenuFeature.cs new file mode 100644 index 0000000..515f3a6 --- /dev/null +++ b/PlayerTags/Features/CustomTagsContextMenuFeature.cs @@ -0,0 +1,97 @@ +using PlayerTags.Configuration; +using PlayerTags.Data; +using PlayerTags.Resources; +using System; +using System.Linq; +using XivCommon; +using XivCommon.Functions.ContextMenu; + +namespace PlayerTags.Features +{ + public class CustomTagsContextMenuFeature : IDisposable + { + private string?[] CustomTagsSupportedAddonNames = new string?[] + { + null, + "_PartyList", + "ChatLog", + "ContactList", + "ContentMemberList", + "CrossWorldLinkshell", + "FreeCompany", + "FriendList", + "LookingForGroup", + "LinkShell", + "PartyMemberList", + "SocialList", + }; + + private XivCommonBase m_XivCommon; + private PluginConfiguration m_PluginConfiguration; + private PluginData m_PluginData; + + public CustomTagsContextMenuFeature(XivCommonBase xivCommon, PluginConfiguration pluginConfiguration, PluginData pluginData) + { + m_XivCommon = xivCommon; + m_PluginConfiguration = pluginConfiguration; + m_PluginData = pluginData; + + m_XivCommon.Functions.ContextMenu.OpenContextMenu += ContextMenu_OpenContextMenu; + } + + public void Dispose() + { + m_XivCommon.Functions.ContextMenu.OpenContextMenu -= ContextMenu_OpenContextMenu; + } + + private void ContextMenu_OpenContextMenu(ContextMenuOpenArgs args) + { + if (!m_PluginConfiguration.IsCustomTagsContextMenuEnabled || !DoesContextMenuSupportCustomTags(args)) + { + return; + } + + string gameObjectName = args.Text!.TextValue; + + var notAddedTags = m_PluginData.CustomTags.Where(tag => !tag.IncludesGameObjectNameToApplyTo(gameObjectName)); + if (notAddedTags.Any()) + { + args.Items.Add(new NormalContextSubMenuItem(Strings.Loc_Static_ContextMenu_AddTag, (itemArgs => + { + foreach (var notAddedTag in notAddedTags) + { + itemArgs.Items.Add(new NormalContextMenuItem(notAddedTag.Text.Value, (args => + { + notAddedTag.AddGameObjectNameToApplyTo(gameObjectName); + }))); + } + }))); + } + + var addedTags = m_PluginData.CustomTags.Where(tag => tag.IncludesGameObjectNameToApplyTo(gameObjectName)); + if (addedTags.Any()) + { + args.Items.Add(new NormalContextSubMenuItem(Strings.Loc_Static_ContextMenu_RemoveTag, (itemArgs => + { + foreach (var addedTag in addedTags) + { + itemArgs.Items.Add(new NormalContextMenuItem(addedTag.Text.Value, (args => + { + addedTag.RemoveGameObjectNameToApplyTo(gameObjectName); + }))); + } + }))); + } + } + + private bool DoesContextMenuSupportCustomTags(BaseContextMenuArgs args) + { + if (args.Text == null || args.ObjectWorld == 0 || args.ObjectWorld == 65535) + { + return false; + } + + return CustomTagsSupportedAddonNames.Contains(args.ParentAddonName); + } + } +} diff --git a/PlayerTags/Features/NameplatesTagTargetFeature.cs b/PlayerTags/Features/NameplatesTagTargetFeature.cs new file mode 100644 index 0000000..8ff3dd6 --- /dev/null +++ b/PlayerTags/Features/NameplatesTagTargetFeature.cs @@ -0,0 +1,252 @@ +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.Text.SeStringHandling.Payloads; +using PlayerTags.Configuration; +using PlayerTags.Data; +using PlayerTags.Hooks; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace PlayerTags.Features +{ + public class NameplatesTagTargetFeature : TagTargetFeature + { + private PluginConfiguration m_PluginConfiguration; + private PluginData m_PluginData; + + private NameplateHooks? m_NameplateHooks; + + public NameplatesTagTargetFeature(PluginConfiguration pluginConfiguration, PluginData pluginData) + : base(pluginConfiguration) + { + m_PluginConfiguration = pluginConfiguration; + m_PluginData = pluginData; + + PluginServices.ClientState.Login += ClientState_Login; + PluginServices.ClientState.Logout += ClientState_Logout; + Hook(); + } + + public override void Dispose() + { + Unhook(); + PluginServices.ClientState.Logout -= ClientState_Logout; + PluginServices.ClientState.Login -= ClientState_Login; + base.Dispose(); + } + + private void Hook() + { + if (m_NameplateHooks == null) + { + m_NameplateHooks = new NameplateHooks(SetPlayerNameplate); + } + } + + private void Unhook() + { + if (m_NameplateHooks != null) + { + m_NameplateHooks.Dispose(); + m_NameplateHooks = null; + } + } + + private void ClientState_Login(object? sender, EventArgs e) + { + Hook(); + } + + private void ClientState_Logout(object? sender, EventArgs e) + { + Unhook(); + } + + protected override BitmapFontIcon? GetIcon(Tag tag) + { + if (tag.IsIconVisibleInNameplates.InheritedValue != null && tag.IsIconVisibleInNameplates.InheritedValue.Value) + { + return tag.Icon.InheritedValue; + } + + return null; + } + + protected override string? GetText(Tag tag) + { + if (tag.IsTextVisibleInNameplates.InheritedValue != null && tag.IsTextVisibleInNameplates.InheritedValue.Value) + { + return tag.Text.InheritedValue; + } + + return null; + } + + /// + /// Sets the strings on a nameplate. + /// + /// The player character context. + /// The name text. + /// The title text. + /// The free company text. + /// Whether the title is visible. + /// Whether the title is above the name or below it. + /// The icon id. + /// Whether the name was changed. + /// Whether the title was changed. + /// Whether the free company was changed. + private void SetPlayerNameplate(PlayerCharacter playerCharacter, SeString name, SeString title, SeString freeCompany, ref bool isTitleVisible, ref bool isTitleAboveName, ref int iconId, out bool isNameChanged, out bool isTitleChanged, out bool isFreeCompanyChanged) + { + AddTagsToNameplate(playerCharacter, name, title, freeCompany, out isNameChanged, out isTitleChanged, out isFreeCompanyChanged); + + if (m_PluginConfiguration.NameplateTitlePosition == NameplateTitlePosition.AlwaysAboveName) + { + isTitleAboveName = true; + } + else if (m_PluginConfiguration.NameplateTitlePosition == NameplateTitlePosition.AlwaysBelowName) + { + isTitleAboveName = false; + } + + if (m_PluginConfiguration.NameplateTitleVisibility == NameplateTitleVisibility.Default) + { + } + else if (m_PluginConfiguration.NameplateTitleVisibility == NameplateTitleVisibility.Always) + { + isTitleVisible = true; + } + else if (m_PluginConfiguration.NameplateTitleVisibility == NameplateTitleVisibility.Never) + { + isTitleVisible = false; + } + else if (m_PluginConfiguration.NameplateTitleVisibility == NameplateTitleVisibility.WhenHasTags) + { + isTitleVisible = isTitleChanged; + } + + if (m_PluginConfiguration.NameplateFreeCompanyVisibility == NameplateFreeCompanyVisibility.Default) + { + } + else if (m_PluginConfiguration.NameplateFreeCompanyVisibility == NameplateFreeCompanyVisibility.Never) + { + freeCompany.Payloads.Clear(); + isFreeCompanyChanged = true; + } + } + + /// + /// Adds the given payload changes to the dictionary. + /// + /// The nameplate element to add changes to. + /// The position to add changes to. + /// The payloads to add. + /// The dictionary to add the changes to. + private void AddPayloadChanges(NameplateElement nameplateElement, TagPosition tagPosition, IEnumerable payloads, Dictionary>> nameplateChanges) + { + if (!payloads.Any()) + { + return; + } + + if (!nameplateChanges.Keys.Contains(nameplateElement)) + { + nameplateChanges[nameplateElement] = new Dictionary>(); + } + + AddPayloadChanges(tagPosition, payloads, nameplateChanges[nameplateElement]); + } + + /// + /// Adds all configured tags to the nameplate of a game object. + /// + /// The game object context. + /// The name text to change. + /// The title text to change. + /// The free company text to change. + /// Whether the name was changed. + /// Whether the title was changed. + /// Whether the free company was changed. + private void AddTagsToNameplate(GameObject gameObject, SeString name, SeString title, SeString freeCompany, out bool isNameChanged, out bool isTitleChanged, out bool isFreeCompanyChanged) + { + isNameChanged = false; + isTitleChanged = false; + isFreeCompanyChanged = false; + + Dictionary>> nameplateChanges = new Dictionary>>(); + + if (gameObject is Character character) + { + // Add the job tag + if (m_PluginData.JobTags.TryGetValue(character.ClassJob.GameData.Abbreviation, out var jobTag)) + { + if (jobTag.TagTargetInNameplates.InheritedValue != null && jobTag.TagPositionInNameplates.InheritedValue != null) + { + var payloads = GetPayloads(jobTag); + if (payloads.Any()) + { + AddPayloadChanges(jobTag.TagTargetInNameplates.InheritedValue.Value, jobTag.TagPositionInNameplates.InheritedValue.Value, payloads, nameplateChanges); + } + } + } + + // Add the randomly generated name tag payload + if (m_PluginConfiguration.IsPlayerNameRandomlyGenerated) + { + var characterName = character.Name.TextValue; + if (characterName != null) + { + var generatedName = RandomNameGenerator.Generate(characterName); + if (generatedName != null) + { + AddPayloadChanges(NameplateElement.Name, TagPosition.Replace, Enumerable.Empty().Append(new TextPayload(generatedName)), nameplateChanges); + } + } + } + } + + // Add the custom tag payloads + foreach (var customTag in m_PluginData.CustomTags) + { + if (customTag.TagTargetInNameplates.InheritedValue != null && customTag.TagPositionInNameplates.InheritedValue != null) + { + if (customTag.IncludesGameObjectNameToApplyTo(gameObject.Name.TextValue)) + { + var payloads = GetPayloads(customTag); + if (payloads.Any()) + { + AddPayloadChanges(customTag.TagTargetInNameplates.InheritedValue.Value, customTag.TagPositionInNameplates.InheritedValue.Value, payloads, nameplateChanges); + } + } + } + } + + // Build the final strings out of the payloads + foreach ((var nameplateElement, var stringChanges) in nameplateChanges) + { + SeString? seString = null; + 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) + { + ApplyStringChanges(seString, stringChanges); + } + } + } + } +} diff --git a/PlayerTags/Features/TagTargetFeature.cs b/PlayerTags/Features/TagTargetFeature.cs new file mode 100644 index 0000000..6734fa4 --- /dev/null +++ b/PlayerTags/Features/TagTargetFeature.cs @@ -0,0 +1,272 @@ +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.Text.SeStringHandling.Payloads; +using PlayerTags.Configuration; +using PlayerTags.Data; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace PlayerTags.Features +{ + public abstract class TagTargetFeature : IDisposable + { + private PluginConfiguration m_PluginConfiguration; + + private Dictionary m_TagPayloads; + private TextPayload m_SpaceTextPayload; + + public TagTargetFeature(PluginConfiguration pluginConfiguration) + { + m_PluginConfiguration = pluginConfiguration; + + m_TagPayloads = new Dictionary(); + m_SpaceTextPayload = new TextPayload($" "); + + m_PluginConfiguration.Saved += PluginConfiguration_Saved; + } + + public virtual void Dispose() + { + m_PluginConfiguration.Saved -= PluginConfiguration_Saved; + } + + protected abstract BitmapFontIcon? GetIcon(Tag tag); + + protected abstract string? GetText(Tag tag); + + private void PluginConfiguration_Saved() + { + // Invalidate the cached payloads so they get remade + m_TagPayloads.Clear(); + } + + /// + /// Gets the payloads for the given tag. If the payloads don't yet exist then they will be created. + /// + /// The tag config to get payloads for. + /// A list of payloads for the given tag. + protected IEnumerable GetPayloads(Tag tag) + { + if (m_TagPayloads.TryGetValue(tag, out var payloads)) + { + return payloads; + } + + m_TagPayloads[tag] = CreatePayloads(tag); + return m_TagPayloads[tag]; + } + + private Payload[] CreatePayloads(Tag tag) + { + List newPayloads = new List(); + + BitmapFontIcon? icon = GetIcon(tag); + if (icon != null && icon.Value != BitmapFontIcon.None) + { + newPayloads.Add(new IconPayload(icon.Value)); + } + + string? text = GetText(tag); + if (!string.IsNullOrWhiteSpace(text)) + { + if (tag.IsTextItalic.InheritedValue != null && tag.IsTextItalic.InheritedValue.Value) + { + newPayloads.Add(new EmphasisItalicPayload(true)); + } + + if (tag.TextGlowColor.InheritedValue != null) + { + newPayloads.Add(new UIGlowPayload(tag.TextGlowColor.InheritedValue.Value)); + } + + if (tag.TextColor.InheritedValue != null) + { + newPayloads.Add(new UIForegroundPayload(tag.TextColor.InheritedValue.Value)); + } + + newPayloads.Add(new TextPayload(text)); + + if (tag.TextColor.InheritedValue != null) + { + newPayloads.Add(new UIForegroundPayload(0)); + } + + if (tag.TextGlowColor.InheritedValue != null) + { + newPayloads.Add(new UIGlowPayload(0)); + } + + if (tag.IsTextItalic.InheritedValue != null && tag.IsTextItalic.InheritedValue.Value) + { + newPayloads.Add(new EmphasisItalicPayload(false)); + } + } + + return newPayloads.ToArray(); + } + + /// + /// Adds an additional space text payload in between any existing text payloads. If there is an icon payload between two text payloads then the space is skipped. + /// Also adds an extra space to the beginning or end depending on the tag position and whether the most significant payload in either direction is a text payload. + /// In spirit, this is to ensure there is always a space between 2 text payloads, including between these payloads and the target payload. + /// + /// The payloads to add spaces between. + private void AddSpacesBetweenTextPayloads(List payloads, TagPosition tagPosition) + { + if (payloads == null) + { + return; + } + + if (!payloads.Any()) + { + return; + } + + List indicesToInsertSpacesAt = new List(); + int lastTextPayloadIndex = -1; + foreach (var payload in payloads.Reverse()) + { + if (payload is IconPayload iconPayload) + { + lastTextPayloadIndex = -1; + } + else if (payload is TextPayload textPayload) + { + if (lastTextPayloadIndex != -1) + { + indicesToInsertSpacesAt.Add(payloads.IndexOf(textPayload) + 1); + } + + lastTextPayloadIndex = payloads.IndexOf(textPayload); + } + } + + foreach (var indexToInsertSpaceAt in indicesToInsertSpacesAt) + { + payloads.Insert(indexToInsertSpaceAt, m_SpaceTextPayload); + } + + // Decide whether to add a space to the end + if (tagPosition == TagPosition.Before) + { + var significantPayloads = payloads.Where(payload => payload is TextPayload || payload is IconPayload); + if (significantPayloads.Last() is TextPayload) + { + payloads.Add(m_SpaceTextPayload); + } + } + // Decide whether to add a space to the beginning + else if (tagPosition == TagPosition.After) + { + var significantPayloads = payloads.Where(payload => payload is TextPayload || payload is IconPayload); + if (significantPayloads.First() is TextPayload) + { + payloads.Insert(0, m_SpaceTextPayload); + } + } + } + + /// + /// Adds the given payload changes to the dictionary. + /// + /// The position to add changes to. + /// The payloads to add. + /// The dictionary to add the changes to. + protected void AddPayloadChanges(TagPosition tagPosition, IEnumerable payloads, Dictionary> stringChanges) + { + if (payloads == null || !payloads.Any()) + { + return; + } + + if (stringChanges == null) + { + return; + } + + if (!stringChanges.Keys.Contains(tagPosition)) + { + stringChanges[tagPosition] = new List(); + } + + stringChanges[tagPosition].AddRange(payloads); + } + + /// + /// Applies changes to the given string. + /// + /// The string to apply changes to. + /// The changes to apply. + /// The payload in the string that changes should be anchored to. If there is no anchor, the changes will be applied to the entire string. + protected void ApplyStringChanges(SeString seString, Dictionary> stringChanges, Payload? anchorPayload = null) + { + if (stringChanges.Count == 0) + { + return; + } + + List tagPositionsOrdered = new List(); + // If there's no anchor payload, do replaces first so that befores and afters are based on the replaced data + if (anchorPayload == null) + { + tagPositionsOrdered.Add(TagPosition.Replace); + } + + tagPositionsOrdered.Add(TagPosition.Before); + tagPositionsOrdered.Add(TagPosition.After); + + // If there is an anchor payload, do replaces last so that we still know which payload needs to be removed + if (anchorPayload != null) + { + tagPositionsOrdered.Add(TagPosition.Replace); + } + + foreach (var tagPosition in tagPositionsOrdered) + { + if (stringChanges.TryGetValue(tagPosition, out var payloads) && payloads.Any()) + { + AddSpacesBetweenTextPayloads(stringChanges[tagPosition], tagPosition); + if (tagPosition == TagPosition.Before) + { + if (anchorPayload != null) + { + var anchorPayloadIndex = seString.Payloads.IndexOf(anchorPayload); + seString.Payloads.InsertRange(anchorPayloadIndex, payloads); + } + else + { + seString.Payloads.InsertRange(0, payloads); + } + } + else if (tagPosition == TagPosition.After) + { + if (anchorPayload != null) + { + var anchorPayloadIndex = seString.Payloads.IndexOf(anchorPayload); + seString.Payloads.InsertRange(anchorPayloadIndex + 1, payloads); + } + else + { + seString.Payloads.AddRange(payloads); + } + } + else if (tagPosition == TagPosition.Replace) + { + if (anchorPayload != null) + { + var anchorPayloadIndex = seString.Payloads.IndexOf(anchorPayload); + seString.Payloads.InsertRange(anchorPayloadIndex, payloads); + seString.Payloads.Remove(anchorPayload); + } + else + { + seString.Payloads.Clear(); + seString.Payloads.AddRange(payloads); + } + } + } + } + } + } +} diff --git a/PlayerTags/PluginHooks.cs b/PlayerTags/Hooks/NameplateHooks.cs similarity index 87% rename from PlayerTags/PluginHooks.cs rename to PlayerTags/Hooks/NameplateHooks.cs index 014f14e..da7ce3d 100644 --- a/PlayerTags/PluginHooks.cs +++ b/PlayerTags/Hooks/NameplateHooks.cs @@ -1,52 +1,42 @@ using Dalamud.Game; -using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Game.Gui; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Hooking; using Dalamud.Logging; using FFXIVClientStructs.FFXIV.Client.UI; using System; -using System.Linq; using System.Runtime.InteropServices; -namespace PlayerTags +namespace PlayerTags.Hooks { - public class PluginHooks : IDisposable + public class NameplateHooks : IDisposable { private class PluginAddressResolver : BaseAddressResolver { - private const string SetNameplateSignature = "48 89 5C 24 ?? 48 89 6C 24 ?? 56 57 41 54 41 56 41 57 48 83 EC 40 44 0F B6 E2"; - internal IntPtr SetNameplatePtr; + 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) { - SetNameplatePtr = scanner.ScanText(SetNameplateSignature); + SetPlayerNameplatePtr = scanner.ScanText(SetPlayerNameplateSignature); } } private delegate IntPtr SetPlayerNameplateDelegate_Unmanaged(IntPtr playerNameplateObjectPtr, bool isTitleAboveName, bool isTitleVisible, IntPtr titlePtr, IntPtr namePtr, IntPtr freeCompanyPtr, int iconId); - private Framework m_Framework; - private ObjectTable m_ObjectTable; - private GameGui m_GameGui; private SetPlayerNameplateDelegate m_SetPlayerNameplate; - private PluginAddressResolver m_PluginAddressResolver; private Hook m_SetPlayerNameplateHook; - public PluginHooks(Framework framework, ObjectTable objectTable, GameGui gameGui, SetPlayerNameplateDelegate setPlayerNameplate) + public NameplateHooks(SetPlayerNameplateDelegate setPlayerNameplate) { - m_Framework = framework; - m_ObjectTable = objectTable; - m_GameGui = gameGui; m_SetPlayerNameplate = setPlayerNameplate; m_PluginAddressResolver = new PluginAddressResolver(); m_PluginAddressResolver.Setup(); - m_SetPlayerNameplateHook = new Hook(m_PluginAddressResolver.SetNameplatePtr, new SetPlayerNameplateDelegate_Unmanaged(SetPlayerNameplateDetour)); + m_SetPlayerNameplateHook = new Hook(m_PluginAddressResolver.SetPlayerNameplatePtr, new SetPlayerNameplateDelegate_Unmanaged(SetPlayerNameplateDetour)); m_SetPlayerNameplateHook.Enable(); } @@ -164,7 +154,7 @@ namespace PlayerTags where T : GameObject { // Get the nameplate object array - var nameplateAddonPtr = m_GameGui.GetAddonByName("NamePlate", 1); + 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) @@ -195,7 +185,7 @@ namespace PlayerTags // Return the object for its object id var objectId = namePlateInfo.ObjectID.ObjectID; - return m_ObjectTable.SearchById(objectId) as T; + return PluginServices.ObjectTable.SearchById(objectId) as T; } } } diff --git a/PlayerTags/SetPlayerNameplateDelegate.cs b/PlayerTags/Hooks/SetPlayerNameplateDelegate.cs similarity index 93% rename from PlayerTags/SetPlayerNameplateDelegate.cs rename to PlayerTags/Hooks/SetPlayerNameplateDelegate.cs index 5e38761..314b6e0 100644 --- a/PlayerTags/SetPlayerNameplateDelegate.cs +++ b/PlayerTags/Hooks/SetPlayerNameplateDelegate.cs @@ -1,7 +1,7 @@ using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.Text.SeStringHandling; -namespace PlayerTags +namespace PlayerTags.Hooks { public delegate void SetPlayerNameplateDelegate(PlayerCharacter playerCharacter, SeString name, SeString title, SeString freeCompany, ref bool isTitleVisible, ref bool isTitleAboveName, ref int iconId, out bool isNameChanged, out bool isTitleChanged, out bool isFreeCompanyChanged); } diff --git a/PlayerTags/IInheritable.cs b/PlayerTags/Inheritables/IInheritable.cs similarity index 88% rename from PlayerTags/IInheritable.cs rename to PlayerTags/Inheritables/IInheritable.cs index acaa423..f20611a 100644 --- a/PlayerTags/IInheritable.cs +++ b/PlayerTags/Inheritables/IInheritable.cs @@ -1,4 +1,4 @@ -namespace PlayerTags +namespace PlayerTags.Inheritables { public interface IInheritable { diff --git a/PlayerTags/InheritableBehavior.cs b/PlayerTags/Inheritables/InheritableBehavior.cs similarity index 73% rename from PlayerTags/InheritableBehavior.cs rename to PlayerTags/Inheritables/InheritableBehavior.cs index f75b2af..752c2e1 100644 --- a/PlayerTags/InheritableBehavior.cs +++ b/PlayerTags/Inheritables/InheritableBehavior.cs @@ -1,4 +1,4 @@ -namespace PlayerTags +namespace PlayerTags.Inheritables { public enum InheritableBehavior { diff --git a/PlayerTags/InheritableData.cs b/PlayerTags/Inheritables/InheritableData.cs similarity index 91% rename from PlayerTags/InheritableData.cs rename to PlayerTags/Inheritables/InheritableData.cs index 98f25c2..88d8d9b 100644 --- a/PlayerTags/InheritableData.cs +++ b/PlayerTags/Inheritables/InheritableData.cs @@ -2,7 +2,7 @@ using Newtonsoft.Json.Converters; using System; -namespace PlayerTags +namespace PlayerTags.Inheritables { [Serializable] public struct InheritableData diff --git a/PlayerTags/InheritableReference.cs b/PlayerTags/Inheritables/InheritableReference.cs similarity index 97% rename from PlayerTags/InheritableReference.cs rename to PlayerTags/Inheritables/InheritableReference.cs index 1342500..abc101f 100644 --- a/PlayerTags/InheritableReference.cs +++ b/PlayerTags/Inheritables/InheritableReference.cs @@ -1,4 +1,4 @@ -namespace PlayerTags +namespace PlayerTags.Inheritables { public class InheritableReference : IInheritable where T : class diff --git a/PlayerTags/InheritableValue.cs b/PlayerTags/Inheritables/InheritableValue.cs similarity index 98% rename from PlayerTags/InheritableValue.cs rename to PlayerTags/Inheritables/InheritableValue.cs index 2c80da3..b62e4b9 100644 --- a/PlayerTags/InheritableValue.cs +++ b/PlayerTags/Inheritables/InheritableValue.cs @@ -1,7 +1,7 @@ using Dalamud.Logging; using System; -namespace PlayerTags +namespace PlayerTags.Inheritables { public class InheritableValue : IInheritable where T : struct diff --git a/PlayerTags/PlayerTags.json b/PlayerTags/PlayerTags.json index a922094..24c67cd 100644 --- a/PlayerTags/PlayerTags.json +++ b/PlayerTags/PlayerTags.json @@ -1,7 +1,7 @@ { "Author": "r00telement", "Name": "Player Tags", - "Description": "See player jobs and roles in nameplates and chat. Create custom tags and add them to players by right clicking them.", + "Description": "Lightweight job visibility in nameplates and chat. Create custom tags and add them to players with the context menu.", "Tags": [ "Jobs", "UI" ], "CategoryTags": [ "jobs", "UI" ], "RepoUrl": "https://github.com/r00telement/PlayerTags", diff --git a/PlayerTags/Plugin.cs b/PlayerTags/Plugin.cs index 260f38a..da5bd29 100644 --- a/PlayerTags/Plugin.cs +++ b/PlayerTags/Plugin.cs @@ -1,24 +1,9 @@ -using Dalamud.Data; -using Dalamud.Game; -using Dalamud.Game.ClientState; -using Dalamud.Game.ClientState.Objects; -using Dalamud.Game.ClientState.Objects.SubKinds; -using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Game.ClientState.Party; -using Dalamud.Game.Command; -using Dalamud.Game.Gui; -using Dalamud.Game.Text; -using Dalamud.Game.Text.SeStringHandling; -using Dalamud.Game.Text.SeStringHandling.Payloads; -using Dalamud.IoC; -using Dalamud.Logging; +using Dalamud.Game.Command; using Dalamud.Plugin; -using PlayerTags.Resources; -using System; -using System.Collections.Generic; -using System.Linq; +using PlayerTags.Configuration; +using PlayerTags.Data; +using PlayerTags.Features; using XivCommon; -using XivCommon.Functions.ContextMenu; namespace PlayerTags { @@ -27,182 +12,56 @@ namespace PlayerTags public string Name => "Player Tags"; private const string c_CommandName = "/playertags"; - [PluginService] - private static DalamudPluginInterface PluginInterface { get; set; } = null!; - - [PluginService] - private static Framework Framework { get; set; } = null!; - - [PluginService] - private static ChatGui ChatGui { get; set; } = null!; - - [PluginService] - private static GameGui GameGui { get; set; } = null!; - - [PluginService] - private static ObjectTable ObjectTable { get; set; } = null!; - - [PluginService] - private static DataManager DataManager { get; set; } = null!; - - [PluginService] - private static CommandManager CommandManager { get; set; } = null!; - - [PluginService] - private static ClientState ClientState { get; set; } = null!; - - [PluginService] - private static PartyList PartyList { get; set; } = null!; + private XivCommonBase m_XivCommon; private PluginConfiguration m_PluginConfiguration; + private PluginData m_PluginData; private PluginConfigurationUI m_PluginConfigurationUI; - private RandomNameGenerator m_RandomNameGenerator = new RandomNameGenerator(); - private PluginHooks? m_PluginHooks = null; - private Dictionary> m_TagTargetPayloads = new Dictionary>(); - private TextPayload m_SpaceTextPayload = new TextPayload($" "); - private PluginData m_PluginData = new PluginData(); - private XivCommonBase XivCommon; - public Plugin() + private CustomTagsContextMenuFeature m_CustomTagsContextMenuFeature; + private NameplatesTagTargetFeature m_NameplatesTagTargetFeature; + private ChatTagTargetFeature m_ChatTagTargetFeature; + + public Plugin(DalamudPluginInterface pluginInterface) { - UIColorHelper.Initialize(DataManager); - m_PluginConfiguration = PluginInterface.GetPluginConfig() as PluginConfiguration ?? new PluginConfiguration(); - m_PluginConfiguration.Initialize(PluginInterface); - m_PluginData.Initialize(DataManager, m_PluginConfiguration); - m_PluginConfigurationUI = new PluginConfigurationUI(m_PluginConfiguration, m_PluginData, ClientState, PartyList, ObjectTable); + PluginServices.Initialize(pluginInterface); - ClientState.Login += ClientState_Login; - ClientState.Logout += ClientState_Logout; - ChatGui.ChatMessage += Chat_ChatMessage; - PluginInterface.UiBuilder.Draw += UiBuilder_Draw; - PluginInterface.UiBuilder.OpenConfigUi += UiBuilder_OpenConfigUi; - m_PluginConfiguration.Saved += PluginConfiguration_Saved; - CommandManager.AddHandler(c_CommandName, new CommandInfo((string command, string arguments) => + m_PluginConfiguration = PluginServices.DalamudPluginInterface.GetPluginConfig() as PluginConfiguration ?? new PluginConfiguration(); + m_PluginData = new PluginData(m_PluginConfiguration); + m_PluginConfigurationUI = new PluginConfigurationUI(m_PluginConfiguration, m_PluginData); + + m_XivCommon = new XivCommonBase(XivCommon.Hooks.ContextMenu); + PluginServices.ClientState.TerritoryChanged += ClientState_TerritoryChanged; + 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); - }) - { - HelpMessage = "Shows the config" - }); - Hook(); - XivCommon = new XivCommonBase(Hooks.ContextMenu); - XivCommon.Functions.ContextMenu.OpenContextMenu += ContextMenu_OpenContextMenu; + }) { HelpMessage = "Shows the config" }); + m_CustomTagsContextMenuFeature = new CustomTagsContextMenuFeature(m_XivCommon, m_PluginConfiguration, m_PluginData); + m_NameplatesTagTargetFeature = new NameplatesTagTargetFeature(m_PluginConfiguration, m_PluginData); + m_ChatTagTargetFeature = new ChatTagTargetFeature(m_PluginConfiguration, m_PluginData); + } + + //private ExcelSheet _contentFinderConditionsSheet; + private void ClientState_TerritoryChanged(object? sender, ushort e) + { + //_contentFinderConditionsSheet = DataManager.GameData.GetExcelSheet(); + //var content = _contentFinderConditionsSheet.FirstOrDefault(t => t.TerritoryType.Row == PluginServices.ClientState.TerritoryType); + //content.ContentMemberType.Row } public void Dispose() { - XivCommon.Functions.ContextMenu.OpenContextMenu -= ContextMenu_OpenContextMenu; - XivCommon.Dispose(); - Unhook(); - CommandManager.RemoveHandler(c_CommandName); - m_PluginConfiguration.Saved -= PluginConfiguration_Saved; - PluginInterface.UiBuilder.OpenConfigUi -= UiBuilder_OpenConfigUi; - PluginInterface.UiBuilder.Draw -= UiBuilder_Draw; - ChatGui.ChatMessage -= Chat_ChatMessage; - ClientState.Logout -= ClientState_Logout; - ClientState.Login -= ClientState_Login; - } - - private void Hook() - { - if (m_PluginHooks == null) - { - m_PluginHooks = new PluginHooks(Framework, ObjectTable, GameGui, SetPlayerNameplate); - } - } - - private void Unhook() - { - if (m_PluginHooks != null) - { - m_PluginHooks.Dispose(); - m_PluginHooks = null; - } - } - - private void ClientState_Login(object? sender, EventArgs e) - { - Hook(); - } - - private void ClientState_Logout(object? sender, EventArgs e) - { - Unhook(); - } - - private void PluginConfiguration_Saved() - { - // Invalidate the cached payloads so they get remade - m_TagTargetPayloads.Clear(); - } - - private void ContextMenu_OpenContextMenu(ContextMenuOpenArgs args) - { - if (!m_PluginConfiguration.IsCustomTagContextMenuEnabled || !CanContextMenuSupportTagOptions(args)) - { - return; - } - - string gameObjectName = args.Text!.TextValue; - - var notAddedTags = m_PluginData.CustomTags.Where(tag => !tag.IncludesGameObjectNameToApplyTo(gameObjectName)); - if (notAddedTags.Any()) - { - args.Items.Add(new NormalContextSubMenuItem(Strings.Loc_Static_ContextMenu_AddTag, (itemArgs => - { - foreach (var notAddedTag in notAddedTags) - { - itemArgs.Items.Add(new NormalContextMenuItem(notAddedTag.Text.Value, (args => - { - notAddedTag.AddGameObjectNameToApplyTo(gameObjectName); - }))); - } - }))); - } - - var addedTags = m_PluginData.CustomTags.Where(tag => tag.IncludesGameObjectNameToApplyTo(gameObjectName)); - if (addedTags.Any()) - { - args.Items.Add(new NormalContextSubMenuItem(Strings.Loc_Static_ContextMenu_RemoveTag, (itemArgs => - { - foreach (var addedTag in addedTags) - { - itemArgs.Items.Add(new NormalContextMenuItem(addedTag.Text.Value, (args => - { - addedTag.RemoveGameObjectNameToApplyTo(gameObjectName); - }))); - } - }))); - } - } - - private bool CanContextMenuSupportTagOptions(BaseContextMenuArgs args) - { - if (args.Text == null || args.ObjectWorld == 0 || args.ObjectWorld == 65535) - { - return false; - } - - switch (args.ParentAddonName) - { - case null: - case "_PartyList": - case "ChatLog": - case "ContactList": - case "ContentMemberList": - case "CrossWorldLinkshell": - case "FreeCompany": - case "FriendList": - case "LookingForGroup": - case "LinkShell": - case "PartyMemberList": - case "SocialList": - return true; - - default: - return false; - } + m_ChatTagTargetFeature.Dispose(); + m_NameplatesTagTargetFeature.Dispose(); + m_CustomTagsContextMenuFeature.Dispose(); + PluginServices.CommandManager.RemoveHandler(c_CommandName); + PluginServices.DalamudPluginInterface.UiBuilder.OpenConfigUi -= UiBuilder_OpenConfigUi; + PluginServices.DalamudPluginInterface.UiBuilder.Draw -= UiBuilder_Draw; + PluginServices.ClientState.TerritoryChanged -= ClientState_TerritoryChanged; + m_XivCommon.Dispose(); } private void UiBuilder_Draw() @@ -218,606 +77,5 @@ namespace PlayerTags m_PluginConfiguration.IsVisible = true; m_PluginConfiguration.Save(m_PluginData); } - - private void Chat_ChatMessage(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled) - { - AddTagsToChat(sender, out _); - AddTagsToChat(message, out _); - } - - /// - /// Sets the strings on a nameplate. - /// - /// The game object context. - /// The name text. - /// The title text. - /// The free company text. - /// Whether the title is visible. - /// Whether the title is above the name or below it. - /// The icon id. - /// Whether the name was changed. - /// Whether the title was changed. - /// Whether the free company was changed. - private void SetPlayerNameplate(PlayerCharacter playerCharacter, SeString name, SeString title, SeString freeCompany, ref bool isTitleVisible, ref bool isTitleAboveName, ref int iconId, out bool isNameChanged, out bool isTitleChanged, out bool isFreeCompanyChanged) - { - AddTagsToNameplate(playerCharacter, name, title, freeCompany, out isNameChanged, out isTitleChanged, out isFreeCompanyChanged); - - if (m_PluginConfiguration.NameplateTitlePosition == NameplateTitlePosition.AlwaysAboveName) - { - isTitleAboveName = true; - } - else if (m_PluginConfiguration.NameplateTitlePosition == NameplateTitlePosition.AlwaysBelowName) - { - isTitleAboveName = false; - } - - if (m_PluginConfiguration.NameplateTitleVisibility == NameplateTitleVisibility.Default) - { - } - else if (m_PluginConfiguration.NameplateTitleVisibility == NameplateTitleVisibility.Always) - { - isTitleVisible = true; - } - else if (m_PluginConfiguration.NameplateTitleVisibility == NameplateTitleVisibility.Never) - { - isTitleVisible = false; - } - else if (m_PluginConfiguration.NameplateTitleVisibility == NameplateTitleVisibility.WhenHasTags) - { - isTitleVisible = isTitleChanged; - } - - if (m_PluginConfiguration.NameplateFreeCompanyVisibility == NameplateFreeCompanyVisibility.Default) - { - } - else if (m_PluginConfiguration.NameplateFreeCompanyVisibility == NameplateFreeCompanyVisibility.Never) - { - freeCompany.Payloads.Clear(); - isFreeCompanyChanged = true; - } - } - - private Payload[] CreateTagPayloads(TagTarget tagTarget, Tag tag) - { - List newPayloads = new List(); - - BitmapFontIcon? icon = null; - if (tagTarget == TagTarget.Chat && tag.IsIconVisibleInChat.InheritedValue != null && tag.IsIconVisibleInChat.InheritedValue.Value) - { - icon = tag.Icon.InheritedValue; - } - else if (tagTarget == TagTarget.Nameplate && tag.IsIconVisibleInNameplates.InheritedValue != null && tag.IsIconVisibleInNameplates.InheritedValue.Value) - { - icon = tag.Icon.InheritedValue; - } - - string? text = null; - if (tagTarget == TagTarget.Chat && tag.IsTextVisibleInChat.InheritedValue != null && tag.IsTextVisibleInChat.InheritedValue.Value) - { - text = tag.Text.InheritedValue; - } - else if (tagTarget == TagTarget.Nameplate && tag.IsTextVisibleInNameplates.InheritedValue != null && tag.IsTextVisibleInNameplates.InheritedValue.Value) - { - text = tag.Text.InheritedValue; - } - - if (!m_TagTargetPayloads.ContainsKey(tag)) - { - m_TagTargetPayloads[tag] = new Dictionary(); - } - - if (icon != null && icon.Value != BitmapFontIcon.None) - { - newPayloads.Add(new IconPayload(icon.Value)); - } - - if (!string.IsNullOrWhiteSpace(text)) - { - if (tag.IsTextItalic.InheritedValue != null && tag.IsTextItalic.InheritedValue.Value) - { - newPayloads.Add(new EmphasisItalicPayload(true)); - } - - if (tag.TextGlowColor.InheritedValue != null) - { - newPayloads.Add(new UIGlowPayload(tag.TextGlowColor.InheritedValue.Value)); - } - - if (tag.TextColor.InheritedValue != null) - { - newPayloads.Add(new UIForegroundPayload(tag.TextColor.InheritedValue.Value)); - } - - newPayloads.Add(new TextPayload(text)); - - if (tag.TextColor.InheritedValue != null) - { - newPayloads.Add(new UIForegroundPayload(0)); - } - - if (tag.TextGlowColor.InheritedValue != null) - { - newPayloads.Add(new UIGlowPayload(0)); - } - - if (tag.IsTextItalic.InheritedValue != null && tag.IsTextItalic.InheritedValue.Value) - { - newPayloads.Add(new EmphasisItalicPayload(false)); - } - } - - return newPayloads.ToArray(); - } - - /// - /// Gets the payloads for the given custom tag. If the payloads don't yet exist then they are created. - /// - /// The custom tag config to get payloads for. - /// A list of payloads for the given custom tag. - private IEnumerable GetTagPayloads(TagTarget tagTarget, Tag tag) - { - if (m_TagTargetPayloads.TryGetValue(tag, out var tagTargetPayloads)) - { - if (tagTargetPayloads.TryGetValue(tagTarget, out var payloads)) - { - return payloads; - } - } - else - { - m_TagTargetPayloads[tag] = new Dictionary(); - } - - m_TagTargetPayloads[tag][tagTarget] = CreateTagPayloads(tagTarget, tag); - return m_TagTargetPayloads[tag][tagTarget]; - } - - /// - /// Adds an additional space text payload in between any existing text payloads. If there is an icon payload between two text payloads then the space is skipped. - /// Also adds an extra space to the beginning or end depending on the tag position and whether the most significant payload in either direction is a text payload. - /// In spirit, this is to ensure there is always a space between 2 text payloads, including between these payloads and the target payload. - /// - /// The payloads to add spaces between. - private void AddSpacesBetweenTextPayloads(List payloads, TagPosition tagPosition) - { - if (payloads == null) - { - return; - } - - if (!payloads.Any()) - { - return; - } - - List indicesToInsertSpacesAt = new List(); - int lastTextPayloadIndex = -1; - foreach (var payload in payloads.Reverse()) - { - if (payload is IconPayload iconPayload) - { - lastTextPayloadIndex = -1; - } - else if (payload is TextPayload textPayload) - { - if (lastTextPayloadIndex != -1) - { - indicesToInsertSpacesAt.Add(payloads.IndexOf(textPayload) + 1); - } - - lastTextPayloadIndex = payloads.IndexOf(textPayload); - } - } - - foreach (var indexToInsertSpaceAt in indicesToInsertSpacesAt) - { - payloads.Insert(indexToInsertSpaceAt, m_SpaceTextPayload); - } - - // Decide whether to add a space to the end - if (tagPosition == TagPosition.Before) - { - var significantPayloads = payloads.Where(payload => payload is TextPayload || payload is IconPayload); - if (significantPayloads.Last() is TextPayload) - { - payloads.Add(m_SpaceTextPayload); - } - } - // Decide whether to add a space to the beginning - else if (tagPosition == TagPosition.After) - { - var significantPayloads = payloads.Where(payload => payload is TextPayload || payload is IconPayload); - if (significantPayloads.First() is TextPayload) - { - payloads.Insert(0, m_SpaceTextPayload); - } - } - } - - /// - /// A match found within a string. - /// - private class StringMatch - { - /// - /// The string that the match was found in. - /// - public SeString SeString { get; init; } - - /// - /// The matching text payload. - /// - public TextPayload TextPayload { get; init; } - - /// - /// The matching game object if one exists - /// - public GameObject? GameObject { get; init; } - - /// - /// A matching player payload if one exists. - /// - public PlayerPayload? PlayerPayload { get; init; } - - public StringMatch(SeString seString, TextPayload textPayload) - { - SeString = seString; - TextPayload = textPayload; - } - - /// - /// Gets the matches text. - /// - /// The match text. - public string GetMatchText() - { - if (GameObject != null) - { - return GameObject.Name.TextValue; - } - - return TextPayload.Text; - } - } - - /// - /// Searches the given string for game object matches. - /// - /// The string to search. - /// A list of matched game objects. - private List GetStringMatches(SeString seString) - { - List stringMatches = new List(); - - for (int payloadIndex = 0; payloadIndex < seString.Payloads.Count; ++payloadIndex) - { - var payload = seString.Payloads[payloadIndex]; - if (payload is PlayerPayload playerPayload) - { - var gameObject = ObjectTable.FirstOrDefault(gameObject => gameObject.Name.TextValue == playerPayload.PlayerName); - - // The next payload MUST be a text payload - if (payloadIndex + 1 < seString.Payloads.Count && seString.Payloads[payloadIndex + 1] is TextPayload textPayload) - { - var stringMatch = new StringMatch(seString, textPayload) - { - GameObject = gameObject, - PlayerPayload = playerPayload - }; - stringMatches.Add(stringMatch); - - // Don't handle the text payload twice - payloadIndex++; - } - else - { - PluginLog.Error("Expected payload after player payload to be a text payload but it wasn't"); - } - } - - /// TODO: Not sure if this is desirable. Enabling this allows tags to appear next to the name of the local player by text in chat because the local player doesn't have a player payload. - /// But because it's just a simple string comparison, it won't work in all circumstances. E.g. in party chat the player name is wrapped in (). To be comprehensive we need to search substring. - /// This means we would need to think about breaking down existing payloads to split them out. - /// If we decide to do that, we could even for example find unlinked player names in chat and add player payloads for them. - // If it's just a text payload then either a character NEEDS to exist for it, or it needs to be identified as a character by custom tag configs - //else if (payload is TextPayload textPayload) - //{ - // var gameObject = ObjectTable.FirstOrDefault(gameObject => gameObject.Name.TextValue == textPayload.Text); - // var isIncludedInCustomTagConfig = m_Config.CustomTags.Any(customTagConfig => customTagConfig.IncludesGameObjectName(textPayload.Text)); - - // if (gameObject != null || isIncludedInCustomTagConfig) - // { - // var stringMatch = new StringMatch(seString, textPayload) - // { - // GameObject = gameObject - // }; - // stringMatches.Add(stringMatch); - // } - //} - } - - return stringMatches; - } - - /// - /// Adds the given payload changes to the dictionary. - /// - /// The position to add changes to. - /// The payloads to add. - /// The dictionary to add the changes to. - private void AddPayloadChanges(TagPosition tagPosition, IEnumerable payloads, Dictionary> stringChanges) - { - if (payloads == null || !payloads.Any()) - { - return; - } - - if (stringChanges == null) - { - return; - } - - if (!stringChanges.Keys.Contains(tagPosition)) - { - stringChanges[tagPosition] = new List(); - } - - stringChanges[tagPosition].AddRange(payloads); - } - - /// - /// Adds the given payload changes to the dictionary. - /// - /// The nameplate element to add changes to. - /// The position to add changes to. - /// The payloads to add. - /// The dictionary to add the changes to. - private void AddPayloadChanges(NameplateElement nameplateElement, TagPosition tagPosition, IEnumerable payloads, Dictionary>> nameplateChanges) - { - if (!payloads.Any()) - { - return; - } - - if (!nameplateChanges.Keys.Contains(nameplateElement)) - { - nameplateChanges[nameplateElement] = new Dictionary>(); - } - - AddPayloadChanges(tagPosition, payloads, nameplateChanges[nameplateElement]); - } - - /// - /// Applies changes to the given string. - /// - /// The string to apply changes to. - /// The changes to apply. - /// The payload in the string that changes should be anchored to. If there is no anchor, the changes will be applied to the entire string. - private void ApplyStringChanges(SeString seString, Dictionary> stringChanges, Payload? anchorPayload = null) - { - if (stringChanges.Count == 0) - { - return; - } - - List tagPositionsOrdered = new List(); - // If there's no anchor payload, do replaces first so that befores and afters are based on the replaced data - if (anchorPayload == null) - { - tagPositionsOrdered.Add(TagPosition.Replace); - } - - tagPositionsOrdered.Add(TagPosition.Before); - tagPositionsOrdered.Add(TagPosition.After); - - // If there is an anchor payload, do replaces last so that we still know which payload needs to be removed - if (anchorPayload != null) - { - tagPositionsOrdered.Add(TagPosition.Replace); - } - - foreach (var tagPosition in tagPositionsOrdered) - { - if (stringChanges.TryGetValue(tagPosition, out var payloads) && payloads.Any()) - { - AddSpacesBetweenTextPayloads(stringChanges[tagPosition], tagPosition); - if (tagPosition == TagPosition.Before) - { - if (anchorPayload != null) - { - var anchorPayloadIndex = seString.Payloads.IndexOf(anchorPayload); - seString.Payloads.InsertRange(anchorPayloadIndex, payloads); - } - else - { - seString.Payloads.InsertRange(0, payloads); - } - } - else if (tagPosition == TagPosition.After) - { - if (anchorPayload != null) - { - var anchorPayloadIndex = seString.Payloads.IndexOf(anchorPayload); - seString.Payloads.InsertRange(anchorPayloadIndex + 1, payloads); - } - else - { - seString.Payloads.AddRange(payloads); - } - } - else if (tagPosition == TagPosition.Replace) - { - if (anchorPayload != null) - { - var anchorPayloadIndex = seString.Payloads.IndexOf(anchorPayload); - seString.Payloads.InsertRange(anchorPayloadIndex, payloads); - seString.Payloads.Remove(anchorPayload); - } - else - { - seString.Payloads.Clear(); - seString.Payloads.AddRange(payloads); - } - } - } - } - } - - /// - /// Adds all configured tags to the nameplate of a game object. - /// - /// The game object context. - /// The name text to change. - /// The title text to change. - /// The free company text to change. - /// Whether the name was changed. - /// Whether the title was changed. - /// Whether the free company was changed. - private void AddTagsToNameplate(GameObject gameObject, SeString name, SeString title, SeString freeCompany, out bool isNameChanged, out bool isTitleChanged, out bool isFreeCompanyChanged) - { - isNameChanged = false; - isTitleChanged = false; - isFreeCompanyChanged = false; - - Dictionary>> nameplateChanges = new Dictionary>>(); - - if (gameObject is Character character) - { - // Add the job tag - if (m_PluginData.JobTags.TryGetValue(character.ClassJob.GameData.Abbreviation, out var jobTag)) - { - if (jobTag.TagTargetInNameplates.InheritedValue != null && jobTag.TagPositionInNameplates.InheritedValue != null) - { - var payloads = GetTagPayloads(TagTarget.Nameplate, jobTag); - if (payloads.Any()) - { - AddPayloadChanges(jobTag.TagTargetInNameplates.InheritedValue.Value, jobTag.TagPositionInNameplates.InheritedValue.Value, payloads, nameplateChanges); - } - } - } - - // Add the randomly generated name tag payload - if (m_PluginConfiguration.IsPlayerNameRandomlyGenerated && m_RandomNameGenerator != null) - { - var characterName = character.Name.TextValue; - if (characterName != null) - { - var generatedName = m_RandomNameGenerator.GetGeneratedName(characterName); - if (generatedName != null) - { - AddPayloadChanges(NameplateElement.Name, TagPosition.Replace, Enumerable.Empty().Append(new TextPayload(generatedName)), nameplateChanges); - } - } - } - } - - // Add the custom tag payloads - foreach (var customTag in m_PluginData.CustomTags) - { - if (customTag.TagTargetInNameplates.InheritedValue != null && customTag.TagPositionInNameplates.InheritedValue != null) - { - if (customTag.IncludesGameObjectNameToApplyTo(gameObject.Name.TextValue)) - { - var payloads = GetTagPayloads(TagTarget.Nameplate, customTag); - if (payloads.Any()) - { - AddPayloadChanges(customTag.TagTargetInNameplates.InheritedValue.Value, customTag.TagPositionInNameplates.InheritedValue.Value, payloads, nameplateChanges); - } - } - } - } - - // Build the final strings out of the payloads - foreach ((var nameplateElement, var stringChanges) in nameplateChanges) - { - SeString? seString = null; - 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) - { - ApplyStringChanges(seString, stringChanges); - } - } - } - - /// - /// Adds all configured tags to chat. - /// - /// The message to change. - /// Whether the message was changed. - private void AddTagsToChat(SeString message, out bool isMessageChanged) - { - isMessageChanged = false; - - var stringMatches = GetStringMatches(message); - foreach (var stringMatch in stringMatches) - { - Dictionary> stringChanges = new Dictionary>(); - - // The role tag payloads - if (stringMatch.GameObject is Character character) - { - // Add the job tag - if (m_PluginData.JobTags.TryGetValue(character.ClassJob.GameData.Abbreviation, out var jobTag)) - { - if (jobTag.TagPositionInChat.InheritedValue != null) - { - var payloads = GetTagPayloads(TagTarget.Chat, jobTag); - if (payloads.Any()) - { - AddPayloadChanges(jobTag.TagPositionInChat.InheritedValue.Value, payloads, stringChanges); - } - } - } - - // Add randomly generated name tag payload - if (m_PluginConfiguration.IsPlayerNameRandomlyGenerated && m_RandomNameGenerator != null) - { - var playerName = stringMatch.GetMatchText(); - if (playerName != null) - { - var generatedName = m_RandomNameGenerator.GetGeneratedName(playerName); - if (generatedName != null) - { - AddPayloadChanges(TagPosition.Replace, Enumerable.Empty().Append(new TextPayload(generatedName)), stringChanges); - } - } - } - } - - // Add the custom tag payloads - foreach (var customTag in m_PluginData.CustomTags) - { - if (customTag.TagPositionInChat.InheritedValue != null) - { - if (customTag.IncludesGameObjectNameToApplyTo(stringMatch.GetMatchText())) - { - var customTagPayloads = GetTagPayloads(TagTarget.Chat, customTag); - if (customTagPayloads.Any()) - { - AddPayloadChanges(customTag.TagPositionInChat.InheritedValue.Value, customTagPayloads, stringChanges); - } - } - } - } - - ApplyStringChanges(message, stringChanges, stringMatch.TextPayload); - isMessageChanged = true; - } - } } } diff --git a/PlayerTags/PluginServices.cs b/PlayerTags/PluginServices.cs new file mode 100644 index 0000000..7728766 --- /dev/null +++ b/PlayerTags/PluginServices.cs @@ -0,0 +1,30 @@ +using Dalamud.Data; +using Dalamud.Game; +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.ClientState.Party; +using Dalamud.Game.Command; +using Dalamud.Game.Gui; +using Dalamud.IoC; +using Dalamud.Plugin; + +namespace PlayerTags +{ + public class PluginServices + { + [PluginService] public static ChatGui ChatGui { get; set; } = null!; + [PluginService] public static ClientState ClientState { get; set; } = null!; + [PluginService] public static CommandManager CommandManager { get; set; } = null!; + [PluginService] public static DalamudPluginInterface DalamudPluginInterface { get; set; } = null!; + [PluginService] public static DataManager DataManager { get; set; } = null!; + [PluginService] public static Framework Framework { get; set; } = null!; + [PluginService] public static GameGui GameGui { get; set; } = null!; + [PluginService] public static ObjectTable ObjectTable { get; set; } = null!; + [PluginService] public static PartyList PartyList { get; set; } = null!; + + public static void Initialize(DalamudPluginInterface pluginInterface) + { + pluginInterface.Create(); + } + } +} diff --git a/PlayerTags/IPluginString.cs b/PlayerTags/PluginStrings/IPluginString.cs similarity index 69% rename from PlayerTags/IPluginString.cs rename to PlayerTags/PluginStrings/IPluginString.cs index d62f7ca..b75aa87 100644 --- a/PlayerTags/IPluginString.cs +++ b/PlayerTags/PluginStrings/IPluginString.cs @@ -1,4 +1,4 @@ -namespace PlayerTags +namespace PlayerTags.PluginStrings { public interface IPluginString { diff --git a/PlayerTags/LiteralPluginString.cs b/PlayerTags/PluginStrings/LiteralPluginString.cs similarity index 89% rename from PlayerTags/LiteralPluginString.cs rename to PlayerTags/PluginStrings/LiteralPluginString.cs index 41ddada..9c91db5 100644 --- a/PlayerTags/LiteralPluginString.cs +++ b/PlayerTags/PluginStrings/LiteralPluginString.cs @@ -1,4 +1,4 @@ -namespace PlayerTags +namespace PlayerTags.PluginStrings { public class LiteralPluginString : IPluginString { diff --git a/PlayerTags/LocalizedPluginString.cs b/PlayerTags/PluginStrings/LocalizedPluginString.cs similarity index 90% rename from PlayerTags/LocalizedPluginString.cs rename to PlayerTags/PluginStrings/LocalizedPluginString.cs index b45a5cd..b8e01bc 100644 --- a/PlayerTags/LocalizedPluginString.cs +++ b/PlayerTags/PluginStrings/LocalizedPluginString.cs @@ -1,4 +1,4 @@ -namespace PlayerTags +namespace PlayerTags.PluginStrings { public class LocalizedPluginString : IPluginString { diff --git a/PlayerTags/RandomNameGenerator.cs b/PlayerTags/RandomNameGenerator.cs index cd065f1..1dd7423 100644 --- a/PlayerTags/RandomNameGenerator.cs +++ b/PlayerTags/RandomNameGenerator.cs @@ -10,31 +10,64 @@ namespace PlayerTags /// /// Generates names based on an existing list of words. /// - public class RandomNameGenerator + public static class RandomNameGenerator { - private const string c_AdjectivesPath = "Resources/Words/Adjectives.txt"; - private string[]? m_Adjectives; - - private const string c_NounsPath = "Resources/Words/Nouns.txt"; - private string[]? m_Nouns; - - private Dictionary m_GeneratedNames = new Dictionary(); - - private string? PluginDirectory + private static string? PluginDirectory { get { return Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); } } - public RandomNameGenerator() + private const string c_AdjectivesPath = "Resources/Words/Adjectives.txt"; + private static string[]? s_Adjectives; + private static string[] Adjectives { - try + get { - m_Adjectives = File.ReadAllLines($"{PluginDirectory}/{c_AdjectivesPath}"); - m_Nouns = File.ReadAllLines($"{PluginDirectory}/{c_NounsPath}"); + if (s_Adjectives == null) + { + try + { + s_Adjectives = File.ReadAllLines($"{PluginDirectory}/{c_AdjectivesPath}"); + } + catch (Exception ex) + { + PluginLog.Error(ex, $"RandomNameGenerator failed to read adjectives"); + } + } + + if (s_Adjectives != null) + { + return s_Adjectives; + } + + return new string[] { }; } - catch (Exception ex) + } + + private const string c_NounsPath = "Resources/Words/Nouns.txt"; + private static string[]? s_Nouns; + private static string[] Nouns + { + get { - PluginLog.Error(ex, $"RandomNameGenerator failed to create"); + if (s_Nouns == null) + { + try + { + s_Nouns = File.ReadAllLines($"{PluginDirectory}/{c_NounsPath}"); + } + catch (Exception ex) + { + PluginLog.Error(ex, $"RandomNameGenerator failed to read nouns"); + } + } + + if (s_Nouns != null) + { + return s_Nouns; + } + + return new string[] { }; } } @@ -43,27 +76,22 @@ namespace PlayerTags /// /// The string to generate a name for. /// A generated name. - public string? GetGeneratedName(string str) + public static string? Generate(string str) { - if (m_Adjectives == null || m_Nouns == null) + if (Adjectives == null || Nouns == null) { return null; } int hash = GetDeterministicHashCode(str); - if (!m_GeneratedNames.ContainsKey(hash)) - { - // Use the seed as the hash so that player always gets the same name - Random random = new Random(hash); - var adjective = m_Adjectives[random.Next(0, m_Adjectives.Length)]; - var noun = m_Nouns[random.Next(0, m_Nouns.Length)]; - var generatedName = $"{adjective} {noun}"; + // Use the seed as the hash so the same player always gets the same name + Random random = new Random(hash); + var adjective = Adjectives[random.Next(0, Adjectives.Length)]; + var noun = Nouns[random.Next(0, Nouns.Length)]; + var generatedName = $"{adjective} {noun}"; - m_GeneratedNames[hash] = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(generatedName); - } - - return m_GeneratedNames[hash]; + return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(generatedName); } /// diff --git a/PlayerTags/Resources/Strings.Designer.cs b/PlayerTags/Resources/Strings.Designer.cs index 2ded504..ae6b367 100644 --- a/PlayerTags/Resources/Strings.Designer.cs +++ b/PlayerTags/Resources/Strings.Designer.cs @@ -135,18 +135,18 @@ namespace PlayerTags.Resources { /// /// Looks up a localized string similar to Context menu integration. /// - public static string Loc_IsCustomTagContextMenuEnabled { + public static string Loc_IsCustomTagsContextMenuEnabled { get { - return ResourceManager.GetString("Loc_IsCustomTagContextMenuEnabled", resourceCulture); + return ResourceManager.GetString("Loc_IsCustomTagsContextMenuEnabled", resourceCulture); } } /// /// Looks up a localized string similar to Options will be available in context menus for adding and removing custom tags from players.. /// - public static string Loc_IsCustomTagContextMenuEnabled_Description { + public static string Loc_IsCustomTagsContextMenuEnabled_Description { get { - return ResourceManager.GetString("Loc_IsCustomTagContextMenuEnabled_Description", resourceCulture); + return ResourceManager.GetString("Loc_IsCustomTagsContextMenuEnabled_Description", resourceCulture); } } diff --git a/PlayerTags/Resources/Strings.resx b/PlayerTags/Resources/Strings.resx index 232c37a..d760a18 100644 --- a/PlayerTags/Resources/Strings.resx +++ b/PlayerTags/Resources/Strings.resx @@ -159,10 +159,10 @@ Expanded - + Context menu integration - + Options will be available in context menus for adding and removing custom tags from players. diff --git a/PlayerTags/UIColorHelper.cs b/PlayerTags/UIColorHelper.cs index b5f66db..827c46b 100644 --- a/PlayerTags/UIColorHelper.cs +++ b/PlayerTags/UIColorHelper.cs @@ -1,5 +1,4 @@ -using Dalamud.Data; -using ImGuiNET; +using ImGuiNET; using Lumina.Excel.GeneratedSheets; using System; using System.Collections.Generic; @@ -28,13 +27,47 @@ namespace PlayerTags } } - private static UIColor[] s_UIColors = new UIColor[] { }; + private static UIColor[] s_UIColors = null!; - public static IEnumerable UIColors { get { return s_UIColors; } } - - public static void Initialize(DataManager dataManager) + public static IEnumerable UIColors { - var uiColors = dataManager.GetExcelSheet(); + get + { + if (s_UIColors == null) + { + s_UIColors = CreateUIColors(); + } + + return s_UIColors; + } + } + + public static Vector4 ToColor(UIColor uiColor) + { + var uiColorBytes = BitConverter.GetBytes(uiColor.UIForeground); + return + new Vector4((float)uiColorBytes[3] / 255, + (float)uiColorBytes[2] / 255, + (float)uiColorBytes[1] / 255, + (float)uiColorBytes[0] / 255); + } + + public static Vector4 ToColor(ushort colorId) + { + foreach (var uiColor in UIColors) + { + if ((ushort)uiColor.RowId == colorId) + { + return ToColor(uiColor); + } + } + + return new Vector4(); + } + + private static UIColor[] CreateUIColors() + { + var uiColors = PluginServices.DataManager.GetExcelSheet(); if (uiColors != null) { var filteredUIColors = new List(uiColors.Distinct(new UIColorComparer()).Where(uiColor => uiColor.UIForeground != 0 && uiColor.UIForeground != 255)); @@ -67,31 +100,10 @@ namespace PlayerTags return 0; }); - s_UIColors = filteredUIColors.ToArray(); - } - } - - public static Vector4 ToColor(UIColor uiColor) - { - var uiColorBytes = BitConverter.GetBytes(uiColor.UIForeground); - return - new Vector4((float)uiColorBytes[3] / 255, - (float)uiColorBytes[2] / 255, - (float)uiColorBytes[1] / 255, - (float)uiColorBytes[0] / 255); - } - - public static Vector4 ToColor(ushort colorId) - { - foreach (var uiColor in UIColors) - { - if ((ushort)uiColor.RowId == colorId) - { - return ToColor(uiColor); - } + return filteredUIColors.ToArray(); } - return new Vector4(); + return new UIColor[] { }; } } }