First pass of reorganizing code

This commit is contained in:
r00telement
2021-12-09 22:09:51 +00:00
parent 78650efdb3
commit 076bde92b2
33 changed files with 1111 additions and 941 deletions

View File

@@ -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)]

View File

@@ -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<ushort>? 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();

View File

@@ -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<byte, Role> RolesById { get; } = new Dictionary<byte, Role>()
public Dictionary<byte, Role> RolesById { get; }
public Dictionary<string, Role> RolesByJobAbbreviation { get; }
public Dictionary<string, InheritableData> AllTagsChanges { get; }
public Dictionary<string, InheritableData> AllRoleTagsChanges { get; }
public Dictionary<Role, Dictionary<string, InheritableData>> RoleTagsChanges { get; }
public Dictionary<string, Dictionary<string, InheritableData>> JobTagsChanges { get; }
public Dictionary<string, InheritableData> AllCustomTagsChanges { get; }
public DefaultPluginData()
{
{ 0, Role.LandHand },
{ 1, Role.Tank },
{ 2, Role.DPS },
{ 3, Role.DPS },
{ 4, Role.Healer },
};
RolesById = new Dictionary<byte, Role>()
{
{ 0, Role.LandHand },
{ 1, Role.Tank },
{ 2, Role.DPS },
{ 3, Role.DPS },
{ 4, Role.Healer },
};
public Dictionary<string, Role> RolesByJobAbbreviation { get; } = new Dictionary<string, Role>();
RolesByJobAbbreviation = new Dictionary<string, Role>();
public Dictionary<string, InheritableData> AllTagsChanges = new Dictionary<string, InheritableData>();
public Dictionary<string, InheritableData> AllRoleTagsChanges = new Dictionary<string, InheritableData>();
public Dictionary<Role, Dictionary<string, InheritableData>> RoleTagsChanges = new Dictionary<Role, Dictionary<string, InheritableData>>();
public Dictionary<string, Dictionary<string, InheritableData>> JobTagsChanges = new Dictionary<string, Dictionary<string, InheritableData>>();
public Dictionary<string, InheritableData> AllCustomTagsChanges = new Dictionary<string, InheritableData>();
public void Initialize(DataManager dataManager)
{
AllTagsChanges = new Tag(new LiteralPluginString(""))
{
IsSelected = true,
@@ -45,6 +50,7 @@ namespace PlayerTags
IsTextVisibleInNameplates = true,
}.GetChanges();
RoleTagsChanges = new Dictionary<Role, Dictionary<string, InheritableData>>();
RoleTagsChanges[Role.LandHand] = new Tag(new LiteralPluginString(""))
{
IsSelected = false,
@@ -77,9 +83,10 @@ namespace PlayerTags
TextColor = 508,
}.GetChanges();
JobTagsChanges = new Dictionary<string, Dictionary<string, InheritableData>>();
foreach ((var role, var roleTagChanges) in RoleTagsChanges)
{
var classJobs = dataManager.GetExcelSheet<ClassJob>();
var classJobs = PluginServices.DataManager.GetExcelSheet<ClassJob>();
if (classJobs == null)
{
break;

View File

@@ -1,4 +1,4 @@
namespace PlayerTags
namespace PlayerTags.Data
{
public enum NameplateElement
{

View File

@@ -1,4 +1,4 @@
namespace PlayerTags
namespace PlayerTags.Data
{
public enum NameplateFreeCompanyVisibility
{

View File

@@ -1,4 +1,4 @@
namespace PlayerTags
namespace PlayerTags.Data
{
public enum NameplateTitlePosition
{

View File

@@ -1,4 +1,4 @@
namespace PlayerTags
namespace PlayerTags.Data
{
public enum NameplateTitleVisibility
{

View File

@@ -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<Tag> 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<Role, Tag>();
JobTags = new Dictionary<string, Tag>();
AllCustomTags = new Tag(new LocalizedPluginString(nameof(AllCustomTags)));
CustomTags = new List<Tag>();
}
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<Role, Tag>();
foreach (var role in Enum.GetValues<Role>())
{
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<Tag>();
AllCustomTags.SetChanges(Default.AllCustomTagsChanges);
AllCustomTags.SetChanges(pluginConfiguration.AllCustomTagsChanges);

View File

@@ -1,4 +1,4 @@
namespace PlayerTags
namespace PlayerTags.Data
{
public enum Role
{

View File

@@ -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
{

View File

@@ -1,4 +1,4 @@
namespace PlayerTags
namespace PlayerTags.Data
{
public enum TagPosition
{

View File

@@ -1,4 +1,4 @@
namespace PlayerTags
namespace PlayerTags.Data
{
public enum TagTarget
{

View File

@@ -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;
}
/// <summary>
/// A match found within a string.
/// </summary>
private class StringMatch
{
/// <summary>
/// The string that the match was found in.
/// </summary>
public SeString SeString { get; init; }
/// <summary>
/// The matching text payload.
/// </summary>
public TextPayload TextPayload { get; init; }
/// <summary>
/// The matching game object if one exists
/// </summary>
public GameObject? GameObject { get; init; }
/// <summary>
/// A matching player payload if one exists.
/// </summary>
public PlayerPayload? PlayerPayload { get; init; }
public StringMatch(SeString seString, TextPayload textPayload)
{
SeString = seString;
TextPayload = textPayload;
}
/// <summary>
/// Gets the matches text.
/// </summary>
/// <returns>The match text.</returns>
public string GetMatchText()
{
if (GameObject != null)
{
return GameObject.Name.TextValue;
}
return TextPayload.Text;
}
}
/// <summary>
/// Searches the given string for game object matches.
/// </summary>
/// <param name="seString">The string to search.</param>
/// <returns>A list of matched game objects.</returns>
private List<StringMatch> GetStringMatches(SeString seString)
{
List<StringMatch> stringMatches = new List<StringMatch>();
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;
}
/// <summary>
/// Adds all configured tags to chat.
/// </summary>
/// <param name="message">The message to change.</param>
/// <param name="isMessageChanged">Whether the message was changed.</param>
private void AddTagsToChat(SeString message, out bool isMessageChanged)
{
isMessageChanged = false;
var stringMatches = GetStringMatches(message);
foreach (var stringMatch in stringMatches)
{
Dictionary<TagPosition, List<Payload>> stringChanges = new Dictionary<TagPosition, List<Payload>>();
// 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<Payload>().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;
}
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
/// <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)
{
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;
}
}
/// <summary>
/// Adds the given payload changes to the dictionary.
/// </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)
{
if (!payloads.Any())
{
return;
}
if (!nameplateChanges.Keys.Contains(nameplateElement))
{
nameplateChanges[nameplateElement] = new Dictionary<TagPosition, List<Payload>>();
}
AddPayloadChanges(tagPosition, payloads, nameplateChanges[nameplateElement]);
}
/// <summary>
/// Adds all configured 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)
{
isNameChanged = false;
isTitleChanged = false;
isFreeCompanyChanged = false;
Dictionary<NameplateElement, Dictionary<TagPosition, List<Payload>>> nameplateChanges = new Dictionary<NameplateElement, Dictionary<TagPosition, List<Payload>>>();
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<Payload>().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);
}
}
}
}
}

View File

@@ -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<Tag, Payload[]> m_TagPayloads;
private TextPayload m_SpaceTextPayload;
public TagTargetFeature(PluginConfiguration pluginConfiguration)
{
m_PluginConfiguration = pluginConfiguration;
m_TagPayloads = new Dictionary<Tag, Payload[]>();
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();
}
/// <summary>
/// Gets the payloads for the given tag. If the payloads don't yet exist then they will be created.
/// </summary>
/// <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)
{
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<Payload> newPayloads = new List<Payload>();
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();
}
/// <summary>
/// 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.
/// </summary>
/// <param name="payloads">The payloads to add spaces between.</param>
private void AddSpacesBetweenTextPayloads(List<Payload> payloads, TagPosition tagPosition)
{
if (payloads == null)
{
return;
}
if (!payloads.Any())
{
return;
}
List<int> indicesToInsertSpacesAt = new List<int>();
int lastTextPayloadIndex = -1;
foreach (var payload in payloads.Reverse<Payload>())
{
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);
}
}
}
/// <summary>
/// Adds the given payload changes to the dictionary.
/// </summary>
/// <param name="tagPosition">The position to add changes to.</param>
/// <param name="payloads">The payloads to add.</param>
/// <param name="stringChanges">The dictionary to add the changes to.</param>
protected void AddPayloadChanges(TagPosition tagPosition, IEnumerable<Payload> payloads, Dictionary<TagPosition, List<Payload>> stringChanges)
{
if (payloads == null || !payloads.Any())
{
return;
}
if (stringChanges == null)
{
return;
}
if (!stringChanges.Keys.Contains(tagPosition))
{
stringChanges[tagPosition] = new List<Payload>();
}
stringChanges[tagPosition].AddRange(payloads);
}
/// <summary>
/// Applies changes to the given string.
/// </summary>
/// <param name="seString">The string to apply changes to.</param>
/// <param name="stringChanges">The changes to apply.</param>
/// <param name="anchorPayload">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.</param>
protected void ApplyStringChanges(SeString seString, Dictionary<TagPosition, List<Payload>> stringChanges, Payload? anchorPayload = null)
{
if (stringChanges.Count == 0)
{
return;
}
List<TagPosition> tagPositionsOrdered = new List<TagPosition>();
// 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);
}
}
}
}
}
}
}

View File

@@ -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<SetPlayerNameplateDelegate_Unmanaged> 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<SetPlayerNameplateDelegate_Unmanaged>(m_PluginAddressResolver.SetNameplatePtr, new SetPlayerNameplateDelegate_Unmanaged(SetPlayerNameplateDetour));
m_SetPlayerNameplateHook = new Hook<SetPlayerNameplateDelegate_Unmanaged>(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;
}
}
}

View File

@@ -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);
}

View File

@@ -1,4 +1,4 @@
namespace PlayerTags
namespace PlayerTags.Inheritables
{
public interface IInheritable
{

View File

@@ -1,4 +1,4 @@
namespace PlayerTags
namespace PlayerTags.Inheritables
{
public enum InheritableBehavior
{

View File

@@ -2,7 +2,7 @@
using Newtonsoft.Json.Converters;
using System;
namespace PlayerTags
namespace PlayerTags.Inheritables
{
[Serializable]
public struct InheritableData

View File

@@ -1,4 +1,4 @@
namespace PlayerTags
namespace PlayerTags.Inheritables
{
public class InheritableReference<T> : IInheritable
where T : class

View File

@@ -1,7 +1,7 @@
using Dalamud.Logging;
using System;
namespace PlayerTags
namespace PlayerTags.Inheritables
{
public class InheritableValue<T> : IInheritable
where T : struct

View File

@@ -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",

View File

@@ -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<Tag, Dictionary<TagTarget, Payload[]>> m_TagTargetPayloads = new Dictionary<Tag, Dictionary<TagTarget, Payload[]>>();
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<ContentFinderCondition> _contentFinderConditionsSheet;
private void ClientState_TerritoryChanged(object? sender, ushort e)
{
//_contentFinderConditionsSheet = DataManager.GameData.GetExcelSheet<ContentFinderCondition>();
//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 _);
}
/// <summary>
/// Sets the strings on a nameplate.
/// </summary>
/// <param name="gameObject">The game object 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)
{
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<Payload> newPayloads = new List<Payload>();
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<TagTarget, Payload[]>();
}
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();
}
/// <summary>
/// Gets the payloads for the given custom tag. If the payloads don't yet exist then they are created.
/// </summary>
/// <param name="customTagConfig">The custom tag config to get payloads for.</param>
/// <returns>A list of payloads for the given custom tag.</returns>
private IEnumerable<Payload> 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<TagTarget, Payload[]>();
}
m_TagTargetPayloads[tag][tagTarget] = CreateTagPayloads(tagTarget, tag);
return m_TagTargetPayloads[tag][tagTarget];
}
/// <summary>
/// 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.
/// </summary>
/// <param name="payloads">The payloads to add spaces between.</param>
private void AddSpacesBetweenTextPayloads(List<Payload> payloads, TagPosition tagPosition)
{
if (payloads == null)
{
return;
}
if (!payloads.Any())
{
return;
}
List<int> indicesToInsertSpacesAt = new List<int>();
int lastTextPayloadIndex = -1;
foreach (var payload in payloads.Reverse<Payload>())
{
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);
}
}
}
/// <summary>
/// A match found within a string.
/// </summary>
private class StringMatch
{
/// <summary>
/// The string that the match was found in.
/// </summary>
public SeString SeString { get; init; }
/// <summary>
/// The matching text payload.
/// </summary>
public TextPayload TextPayload { get; init; }
/// <summary>
/// The matching game object if one exists
/// </summary>
public GameObject? GameObject { get; init; }
/// <summary>
/// A matching player payload if one exists.
/// </summary>
public PlayerPayload? PlayerPayload { get; init; }
public StringMatch(SeString seString, TextPayload textPayload)
{
SeString = seString;
TextPayload = textPayload;
}
/// <summary>
/// Gets the matches text.
/// </summary>
/// <returns>The match text.</returns>
public string GetMatchText()
{
if (GameObject != null)
{
return GameObject.Name.TextValue;
}
return TextPayload.Text;
}
}
/// <summary>
/// Searches the given string for game object matches.
/// </summary>
/// <param name="seString">The string to search.</param>
/// <returns>A list of matched game objects.</returns>
private List<StringMatch> GetStringMatches(SeString seString)
{
List<StringMatch> stringMatches = new List<StringMatch>();
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;
}
/// <summary>
/// Adds the given payload changes to the dictionary.
/// </summary>
/// <param name="tagPosition">The position to add changes to.</param>
/// <param name="payloads">The payloads to add.</param>
/// <param name="stringChanges">The dictionary to add the changes to.</param>
private void AddPayloadChanges(TagPosition tagPosition, IEnumerable<Payload> payloads, Dictionary<TagPosition, List<Payload>> stringChanges)
{
if (payloads == null || !payloads.Any())
{
return;
}
if (stringChanges == null)
{
return;
}
if (!stringChanges.Keys.Contains(tagPosition))
{
stringChanges[tagPosition] = new List<Payload>();
}
stringChanges[tagPosition].AddRange(payloads);
}
/// <summary>
/// Adds the given payload changes to the dictionary.
/// </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)
{
if (!payloads.Any())
{
return;
}
if (!nameplateChanges.Keys.Contains(nameplateElement))
{
nameplateChanges[nameplateElement] = new Dictionary<TagPosition, List<Payload>>();
}
AddPayloadChanges(tagPosition, payloads, nameplateChanges[nameplateElement]);
}
/// <summary>
/// Applies changes to the given string.
/// </summary>
/// <param name="seString">The string to apply changes to.</param>
/// <param name="stringChanges">The changes to apply.</param>
/// <param name="anchorPayload">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.</param>
private void ApplyStringChanges(SeString seString, Dictionary<TagPosition, List<Payload>> stringChanges, Payload? anchorPayload = null)
{
if (stringChanges.Count == 0)
{
return;
}
List<TagPosition> tagPositionsOrdered = new List<TagPosition>();
// 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);
}
}
}
}
}
/// <summary>
/// Adds all configured 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)
{
isNameChanged = false;
isTitleChanged = false;
isFreeCompanyChanged = false;
Dictionary<NameplateElement, Dictionary<TagPosition, List<Payload>>> nameplateChanges = new Dictionary<NameplateElement, Dictionary<TagPosition, List<Payload>>>();
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<Payload>().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);
}
}
}
/// <summary>
/// Adds all configured tags to chat.
/// </summary>
/// <param name="message">The message to change.</param>
/// <param name="isMessageChanged">Whether the message was changed.</param>
private void AddTagsToChat(SeString message, out bool isMessageChanged)
{
isMessageChanged = false;
var stringMatches = GetStringMatches(message);
foreach (var stringMatch in stringMatches)
{
Dictionary<TagPosition, List<Payload>> stringChanges = new Dictionary<TagPosition, List<Payload>>();
// 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<Payload>().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;
}
}
}
}

View File

@@ -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<PluginServices>();
}
}
}

View File

@@ -1,4 +1,4 @@
namespace PlayerTags
namespace PlayerTags.PluginStrings
{
public interface IPluginString
{

View File

@@ -1,4 +1,4 @@
namespace PlayerTags
namespace PlayerTags.PluginStrings
{
public class LiteralPluginString : IPluginString
{

View File

@@ -1,4 +1,4 @@
namespace PlayerTags
namespace PlayerTags.PluginStrings
{
public class LocalizedPluginString : IPluginString
{

View File

@@ -10,31 +10,64 @@ namespace PlayerTags
/// <summary>
/// Generates names based on an existing list of words.
/// </summary>
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<int, string> m_GeneratedNames = new Dictionary<int, string>();
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
/// </summary>
/// <param name="str">The string to generate a name for.</param>
/// <returns>A generated name.</returns>
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);
}
/// <summary>

View File

@@ -135,18 +135,18 @@ namespace PlayerTags.Resources {
/// <summary>
/// Looks up a localized string similar to Context menu integration.
/// </summary>
public static string Loc_IsCustomTagContextMenuEnabled {
public static string Loc_IsCustomTagsContextMenuEnabled {
get {
return ResourceManager.GetString("Loc_IsCustomTagContextMenuEnabled", resourceCulture);
return ResourceManager.GetString("Loc_IsCustomTagsContextMenuEnabled", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Options will be available in context menus for adding and removing custom tags from players..
/// </summary>
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);
}
}

View File

@@ -159,10 +159,10 @@
<data name="Loc_IsExpanded" xml:space="preserve">
<value>Expanded</value>
</data>
<data name="Loc_IsCustomTagContextMenuEnabled" xml:space="preserve">
<data name="Loc_IsCustomTagsContextMenuEnabled" xml:space="preserve">
<value>Context menu integration</value>
</data>
<data name="Loc_IsCustomTagContextMenuEnabled_Description" xml:space="preserve">
<data name="Loc_IsCustomTagsContextMenuEnabled_Description" xml:space="preserve">
<value>Options will be available in context menus for adding and removing custom tags from players.</value>
</data>
<data name="Loc_IsShowInheritedPropertiesEnabled" xml:space="preserve">

View File

@@ -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<UIColor> UIColors { get { return s_UIColors; } }
public static void Initialize(DataManager dataManager)
public static IEnumerable<UIColor> UIColors
{
var uiColors = dataManager.GetExcelSheet<UIColor>();
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<UIColor>();
if (uiColors != null)
{
var filteredUIColors = new List<UIColor>(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[] { };
}
}
}