diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateGui.cs b/Dalamud/Game/Gui/NamePlate/NamePlateGui.cs
new file mode 100644
index 000000000..28e2c36eb
--- /dev/null
+++ b/Dalamud/Game/Gui/NamePlate/NamePlateGui.cs
@@ -0,0 +1,302 @@
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+
+using Dalamud.Game.Addon.Lifecycle;
+using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+using Dalamud.Game.ClientState.Objects;
+using Dalamud.IoC;
+using Dalamud.IoC.Internal;
+using Dalamud.Plugin.Services;
+
+using FFXIVClientStructs.FFXIV.Client.UI;
+
+namespace Dalamud.Game.Gui.NamePlate;
+
+///
+/// Class used to modify the data used when rendering nameplates.
+///
+[ServiceManager.EarlyLoadedService]
+internal sealed class NamePlateGui : IInternalDisposableService, INamePlateGui
+{
+ ///
+ /// The index for the number array used by the NamePlate addon.
+ ///
+ public const int NumberArrayIndex = 5;
+
+ ///
+ /// The index for the string array used by the NamePlate addon.
+ ///
+ public const int StringArrayIndex = 4;
+
+ ///
+ /// The index for of the FullUpdate entry in the NamePlate number array.
+ ///
+ internal const int NumberArrayFullUpdateIndex = 4;
+
+ ///
+ /// An empty null-terminated string pointer allocated in unmanaged memory, used to tag removed fields.
+ ///
+ internal static readonly nint EmptyStringPointer = CreateEmptyStringPointer();
+
+ [ServiceManager.ServiceDependency]
+ private readonly AddonLifecycle addonLifecycle = Service.Get();
+
+ [ServiceManager.ServiceDependency]
+ private readonly GameGui gameGui = Service.Get();
+
+ [ServiceManager.ServiceDependency]
+ private readonly ObjectTable objectTable = Service.Get();
+
+ private readonly AddonLifecycleEventListener preRequestedUpdateListener;
+
+ private NamePlateUpdateContext? context;
+
+ private NamePlateUpdateHandler[] updateHandlers = [];
+
+ [ServiceManager.ServiceConstructor]
+ private NamePlateGui()
+ {
+ this.preRequestedUpdateListener = new AddonLifecycleEventListener(
+ AddonEvent.PreRequestedUpdate,
+ "NamePlate",
+ this.OnPreRequestedUpdate);
+
+ this.addonLifecycle.RegisterListener(this.preRequestedUpdateListener);
+ }
+
+ ///
+ public event INamePlateGui.OnPlateUpdateDelegate? OnNamePlateUpdate;
+
+ ///
+ public event INamePlateGui.OnPlateUpdateDelegate? OnDataUpdate;
+
+ ///
+ public unsafe void RequestRedraw()
+ {
+ var addon = this.gameGui.GetAddonByName("NamePlate");
+ if (addon != 0)
+ {
+ var raptureAtkModule = RaptureAtkModule.Instance();
+ if (raptureAtkModule == null)
+ {
+ return;
+ }
+
+ ((AddonNamePlate*)addon)->DoFullUpdate = 1;
+ var namePlateNumberArrayData = raptureAtkModule->AtkArrayDataHolder.NumberArrays[NumberArrayIndex];
+ namePlateNumberArrayData->SetValue(NumberArrayFullUpdateIndex, 1);
+ }
+ }
+
+ ///
+ void IInternalDisposableService.DisposeService()
+ {
+ this.addonLifecycle.UnregisterListener(this.preRequestedUpdateListener);
+ }
+
+ ///
+ /// Strips the surrounding quotes from a free company tag. If the quotes are not present in the expected location,
+ /// no modifications will be made.
+ ///
+ /// A quoted free company tag.
+ /// A span containing the free company tag without its surrounding quote characters.
+ internal static ReadOnlySpan StripFreeCompanyTagQuotes(ReadOnlySpan text)
+ {
+ if (text.Length > 4 && text.StartsWith(" «"u8) && text.EndsWith("»"u8))
+ {
+ return text[3..^2];
+ }
+
+ return text;
+ }
+
+ ///
+ /// Strips the surrounding quotes from a title. If the quotes are not present in the expected location, no
+ /// modifications will be made.
+ ///
+ /// A quoted title.
+ /// A span containing the title without its surrounding quote characters.
+ internal static ReadOnlySpan StripTitleQuotes(ReadOnlySpan text)
+ {
+ if (text.Length > 5 && text.StartsWith("《"u8) && text.EndsWith("》"u8))
+ {
+ return text[3..^3];
+ }
+
+ return text;
+ }
+
+ private static nint CreateEmptyStringPointer()
+ {
+ var pointer = Marshal.AllocHGlobal(1);
+ Marshal.WriteByte(pointer, 0, 0);
+ return pointer;
+ }
+
+ private void CreateHandlers(NamePlateUpdateContext createdContext)
+ {
+ var handlers = new List();
+ for (var i = 0; i < AddonNamePlate.NumNamePlateObjects; i++)
+ {
+ handlers.Add(new NamePlateUpdateHandler(createdContext, i));
+ }
+
+ this.updateHandlers = handlers.ToArray();
+ }
+
+ private void OnPreRequestedUpdate(AddonEvent type, AddonArgs args)
+ {
+ if (this.OnDataUpdate == null && this.OnNamePlateUpdate == null)
+ {
+ return;
+ }
+
+ var reqArgs = (AddonRequestedUpdateArgs)args;
+ if (this.context == null)
+ {
+ this.context = new NamePlateUpdateContext(this.objectTable, reqArgs);
+ this.CreateHandlers(this.context);
+ }
+ else
+ {
+ this.context.ResetState(reqArgs);
+ }
+
+ var activeNamePlateCount = this.context.ActiveNamePlateCount;
+ if (activeNamePlateCount == 0)
+ return;
+
+ var activeHandlers = this.updateHandlers[..activeNamePlateCount];
+
+ if (this.context.IsFullUpdate)
+ {
+ foreach (var handler in activeHandlers)
+ {
+ handler.ResetState();
+ }
+
+ this.OnDataUpdate?.Invoke(this.context, activeHandlers);
+ this.OnNamePlateUpdate?.Invoke(this.context, activeHandlers);
+ if (this.context.HasParts)
+ this.ApplyBuilders(activeHandlers);
+ }
+ else
+ {
+ var udpatedHandlers = new List(activeNamePlateCount);
+ foreach (var handler in activeHandlers)
+ {
+ handler.ResetState();
+ if (handler.IsUpdating)
+ udpatedHandlers.Add(handler);
+ }
+
+ if (this.OnDataUpdate is not null)
+ {
+ this.OnDataUpdate?.Invoke(this.context, activeHandlers);
+ this.OnNamePlateUpdate?.Invoke(this.context, udpatedHandlers);
+ if (this.context.HasParts)
+ this.ApplyBuilders(activeHandlers);
+ }
+ else if (udpatedHandlers.Count != 0)
+ {
+ var changedHandlersSpan = udpatedHandlers.ToArray().AsSpan();
+ this.OnNamePlateUpdate?.Invoke(this.context, udpatedHandlers);
+ if (this.context.HasParts)
+ this.ApplyBuilders(changedHandlersSpan);
+ }
+ }
+ }
+
+ private void ApplyBuilders(Span handlers)
+ {
+ foreach (var handler in handlers)
+ {
+ if (handler.PartsContainer is { } container)
+ {
+ container.ApplyBuilders(handler);
+ }
+ }
+ }
+}
+
+///
+/// Plugin-scoped version of a AddonEventManager service.
+///
+[PluginInterface]
+[ServiceManager.ScopedService]
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+internal class NamePlateGuiPluginScoped : IInternalDisposableService, INamePlateGui
+{
+ [ServiceManager.ServiceDependency]
+ private readonly NamePlateGui parentService = Service.Get();
+
+ ///
+ public event INamePlateGui.OnPlateUpdateDelegate? OnNamePlateUpdate
+ {
+ add
+ {
+ if (this.OnNamePlateUpdateScoped == null)
+ this.parentService.OnNamePlateUpdate += this.OnNamePlateUpdateForward;
+ this.OnNamePlateUpdateScoped += value;
+ }
+
+ remove
+ {
+ this.OnNamePlateUpdateScoped -= value;
+ if (this.OnNamePlateUpdateScoped == null)
+ this.parentService.OnNamePlateUpdate -= this.OnNamePlateUpdateForward;
+ }
+ }
+
+ ///
+ public event INamePlateGui.OnPlateUpdateDelegate? OnDataUpdate
+ {
+ add
+ {
+ if (this.OnDataUpdateScoped == null)
+ this.parentService.OnDataUpdate += this.OnDataUpdateForward;
+ this.OnDataUpdateScoped += value;
+ }
+
+ remove
+ {
+ this.OnDataUpdateScoped -= value;
+ if (this.OnDataUpdateScoped == null)
+ this.parentService.OnDataUpdate -= this.OnDataUpdateForward;
+ }
+ }
+
+ private event INamePlateGui.OnPlateUpdateDelegate? OnNamePlateUpdateScoped;
+
+ private event INamePlateGui.OnPlateUpdateDelegate? OnDataUpdateScoped;
+
+ ///
+ public void RequestRedraw()
+ {
+ this.parentService.RequestRedraw();
+ }
+
+ ///
+ public void DisposeService()
+ {
+ this.parentService.OnNamePlateUpdate -= this.OnNamePlateUpdateForward;
+ this.OnNamePlateUpdateScoped = null;
+
+ this.parentService.OnDataUpdate -= this.OnDataUpdateForward;
+ this.OnDataUpdateScoped = null;
+ }
+
+ private void OnNamePlateUpdateForward(
+ INamePlateUpdateContext context, IReadOnlyList handlers)
+ {
+ this.OnNamePlateUpdateScoped?.Invoke(context, handlers);
+ }
+
+ private void OnDataUpdateForward(
+ INamePlateUpdateContext context, IReadOnlyList handlers)
+ {
+ this.OnDataUpdateScoped?.Invoke(context, handlers);
+ }
+}
diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateInfoView.cs b/Dalamud/Game/Gui/NamePlate/NamePlateInfoView.cs
new file mode 100644
index 000000000..020905422
--- /dev/null
+++ b/Dalamud/Game/Gui/NamePlate/NamePlateInfoView.cs
@@ -0,0 +1,105 @@
+using Dalamud.Game.Text.SeStringHandling;
+
+using FFXIVClientStructs.FFXIV.Client.UI;
+
+namespace Dalamud.Game.Gui.NamePlate;
+
+///
+/// Provides a read-only view of the nameplate info object data for a nameplate. Modifications to
+/// fields do not affect this data.
+///
+public interface INamePlateInfoView
+{
+ ///
+ /// Gets the displayed name for this nameplate according to the nameplate info object.
+ ///
+ SeString Name { get; }
+
+ ///
+ /// Gets the displayed free company tag for this nameplate according to the nameplate info object. For this field,
+ /// the quote characters which appear on either side of the title are NOT included.
+ ///
+ SeString FreeCompanyTag { get; }
+
+ ///
+ /// Gets the displayed free company tag for this nameplate according to the nameplate info object. For this field,
+ /// the quote characters which appear on either side of the title ARE included.
+ ///
+ SeString QuotedFreeCompanyTag { get; }
+
+ ///
+ /// Gets the displayed title for this nameplate according to the nameplate info object. For this field, the quote
+ /// characters which appear on either side of the title are NOT included.
+ ///
+ SeString Title { get; }
+
+ ///
+ /// Gets the displayed title for this nameplate according to the nameplate info object. For this field, the quote
+ /// characters which appear on either side of the title ARE included.
+ ///
+ SeString QuotedTitle { get; }
+
+ ///
+ /// Gets the displayed level text for this nameplate according to the nameplate info object.
+ ///
+ SeString LevelText { get; }
+
+ ///
+ /// Gets the flags for this nameplate according to the nameplate info object.
+ ///
+ int Flags { get; }
+
+ ///
+ /// Gets a value indicating whether this nameplate is considered 'dirty' or not according to the nameplate
+ /// info object.
+ ///
+ bool IsDirty { get; }
+
+ ///
+ /// Gets a value indicating whether the title for this nameplate is a prefix title or not according to the nameplate
+ /// info object. This value is derived from the field.
+ ///
+ bool IsPrefixTitle { get; }
+}
+
+///
+/// Provides a read-only view of the nameplate info object data for a nameplate. Modifications to
+/// fields do not affect this data.
+///
+internal unsafe class NamePlateInfoView(RaptureAtkModule.NamePlateInfo* info) : INamePlateInfoView
+{
+ private SeString? name;
+ private SeString? freeCompanyTag;
+ private SeString? quotedFreeCompanyTag;
+ private SeString? title;
+ private SeString? quotedTitle;
+ private SeString? levelText;
+
+ ///
+ public SeString Name => this.name ??= SeString.Parse(info->Name);
+
+ ///
+ public SeString FreeCompanyTag => this.freeCompanyTag ??=
+ SeString.Parse(NamePlateGui.StripFreeCompanyTagQuotes(info->FcName));
+
+ ///
+ public SeString QuotedFreeCompanyTag => this.quotedFreeCompanyTag ??= SeString.Parse(info->FcName);
+
+ ///
+ public SeString Title => this.title ??= SeString.Parse(info->Title);
+
+ ///
+ public SeString QuotedTitle => this.quotedTitle ??= SeString.Parse(info->DisplayTitle);
+
+ ///
+ public SeString LevelText => this.levelText ??= SeString.Parse(info->LevelText);
+
+ ///
+ public int Flags => info->Flags;
+
+ ///
+ public bool IsDirty => info->IsDirty;
+
+ ///
+ public bool IsPrefixTitle => ((info->Flags >> (8 * 3)) & 0xFF) == 1;
+}
diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateKind.cs b/Dalamud/Game/Gui/NamePlate/NamePlateKind.cs
new file mode 100644
index 000000000..af41ae199
--- /dev/null
+++ b/Dalamud/Game/Gui/NamePlate/NamePlateKind.cs
@@ -0,0 +1,57 @@
+namespace Dalamud.Game.Gui.NamePlate;
+
+///
+/// An enum describing what kind of game object this nameplate represents.
+///
+public enum NamePlateKind : byte
+{
+ ///
+ /// A player character.
+ ///
+ PlayerCharacter = 0,
+
+ ///
+ /// An event NPC or companion.
+ ///
+ EventNpcCompanion = 1,
+
+ ///
+ /// A retainer.
+ ///
+ Retainer = 2,
+
+ ///
+ /// An enemy battle NPC.
+ ///
+ BattleNpcEnemy = 3,
+
+ ///
+ /// A friendly battle NPC.
+ ///
+ BattleNpcFriendly = 4,
+
+ ///
+ /// An event object.
+ ///
+ EventObject = 5,
+
+ ///
+ /// Treasure.
+ ///
+ Treasure = 6,
+
+ ///
+ /// A gathering point.
+ ///
+ GatheringPoint = 7,
+
+ ///
+ /// A battle NPC with subkind 6.
+ ///
+ BattleNpcSubkind6 = 8,
+
+ ///
+ /// Something else.
+ ///
+ Other = 9,
+}
diff --git a/Dalamud/Game/Gui/NamePlate/NamePlatePartsContainer.cs b/Dalamud/Game/Gui/NamePlate/NamePlatePartsContainer.cs
new file mode 100644
index 000000000..c6f443c91
--- /dev/null
+++ b/Dalamud/Game/Gui/NamePlate/NamePlatePartsContainer.cs
@@ -0,0 +1,46 @@
+namespace Dalamud.Game.Gui.NamePlate;
+
+///
+/// A container for parts.
+///
+internal class NamePlatePartsContainer
+{
+ private NamePlateSimpleParts? nameParts;
+ private NamePlateQuotedParts? titleParts;
+ private NamePlateQuotedParts? freeCompanyTagParts;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The currently executing update context.
+ public NamePlatePartsContainer(NamePlateUpdateContext context)
+ {
+ context.HasParts = true;
+ }
+
+ ///
+ /// Gets a parts object for constructing a nameplate name.
+ ///
+ internal NamePlateSimpleParts Name => this.nameParts ??= new NamePlateSimpleParts(NamePlateStringField.Name);
+
+ ///
+ /// Gets a parts object for constructing a nameplate title.
+ ///
+ internal NamePlateQuotedParts Title => this.titleParts ??= new NamePlateQuotedParts(NamePlateStringField.Title, false);
+
+ ///
+ /// Gets a parts object for constructing a nameplate free company tag.
+ ///
+ internal NamePlateQuotedParts FreeCompanyTag => this.freeCompanyTagParts ??= new NamePlateQuotedParts(NamePlateStringField.FreeCompanyTag, true);
+
+ ///
+ /// Applies all container parts.
+ ///
+ /// The handler to apply the builders to.
+ internal void ApplyBuilders(NamePlateUpdateHandler handler)
+ {
+ this.nameParts?.Apply(handler);
+ this.freeCompanyTagParts?.Apply(handler);
+ this.titleParts?.Apply(handler);
+ }
+}
diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs b/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs
new file mode 100644
index 000000000..e05e553cd
--- /dev/null
+++ b/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs
@@ -0,0 +1,104 @@
+using Dalamud.Game.Text.SeStringHandling;
+
+namespace Dalamud.Game.Gui.NamePlate;
+
+///
+/// A part builder for constructing and setting quoted nameplate fields (i.e. free company tag and title).
+///
+/// The field type which should be set.
+///
+/// This class works as a lazy writer initialized with empty parts, where an empty part signifies no change should be
+/// performed. Only after all handler processing is complete does it write out any parts which were set to the
+/// associated field. Reading fields from this class is usually not what you want to do, as you'll only be reading the
+/// contents of parts which other plugins have written to. Prefer reading from the base handler's properties or using
+/// .
+///
+public class NamePlateQuotedParts(NamePlateStringField field, bool isFreeCompany)
+{
+ ///
+ /// Gets or sets the opening and closing SeStrings which will wrap the entire contents, which can be used to apply
+ /// colors or styling to the entire field.
+ ///
+ public (SeString, SeString)? OuterWrap { get; set; }
+
+ ///
+ /// Gets or sets the opening quote string which appears before the text and opening text-wrap.
+ ///
+ public SeString? LeftQuote { get; set; }
+
+ ///
+ /// Gets or sets the closing quote string which appears after the text and closing text-wrap.
+ ///
+ public SeString? RightQuote { get; set; }
+
+ ///
+ /// Gets or sets the opening and closing SeStrings which will wrap the text, which can be used to apply colors or
+ /// styling to the field's text.
+ ///
+ public (SeString, SeString)? TextWrap { get; set; }
+
+ ///
+ /// Gets or sets this field's text.
+ ///
+ public SeString? Text { get; set; }
+
+ ///
+ /// Applies the changes from this builder to the actual field.
+ ///
+ /// The handler to perform the changes on.
+ internal unsafe void Apply(NamePlateUpdateHandler handler)
+ {
+ if ((nint)handler.GetFieldAsPointer(field) == NamePlateGui.EmptyStringPointer)
+ return;
+
+ var sb = new SeStringBuilder();
+ if (this.OuterWrap is { Item1: var outerLeft })
+ {
+ sb.Append(outerLeft);
+ }
+
+ if (this.LeftQuote is not null)
+ {
+ sb.Append(this.LeftQuote);
+ }
+ else
+ {
+ sb.Append(isFreeCompany ? " «" : "《");
+ }
+
+ if (this.TextWrap is { Item1: var left, Item2: var right })
+ {
+ sb.Append(left);
+ sb.Append(this.Text ?? this.GetStrippedField(handler));
+ sb.Append(right);
+ }
+ else
+ {
+ sb.Append(this.Text ?? this.GetStrippedField(handler));
+ }
+
+ if (this.RightQuote is not null)
+ {
+ sb.Append(this.RightQuote);
+ }
+ else
+ {
+ sb.Append(isFreeCompany ? "»" : "》");
+ }
+
+ if (this.OuterWrap is { Item2: var outerRight })
+ {
+ sb.Append(outerRight);
+ }
+
+ handler.SetField(field, sb.Build());
+ }
+
+ private SeString GetStrippedField(NamePlateUpdateHandler handler)
+ {
+ return SeString.Parse(
+ isFreeCompany
+ ? NamePlateGui.StripFreeCompanyTagQuotes(handler.GetFieldAsSpan(field))
+ : NamePlateGui.StripTitleQuotes(handler.GetFieldAsSpan(field)));
+ }
+}
diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateSimpleParts.cs b/Dalamud/Game/Gui/NamePlate/NamePlateSimpleParts.cs
new file mode 100644
index 000000000..2906005da
--- /dev/null
+++ b/Dalamud/Game/Gui/NamePlate/NamePlateSimpleParts.cs
@@ -0,0 +1,51 @@
+using Dalamud.Game.Text.SeStringHandling;
+
+namespace Dalamud.Game.Gui.NamePlate;
+
+///
+/// A part builder for constructing and setting a simple (unquoted) nameplate field.
+///
+/// The field type which should be set.
+///
+/// This class works as a lazy writer initialized with empty parts, where an empty part signifies no change should be
+/// performed. Only after all handler processing is complete does it write out any parts which were set to the
+/// associated field. Reading fields from this class is usually not what you want to do, as you'll only be reading the
+/// contents of parts which other plugins have written to. Prefer reading from the base handler's properties or using
+/// .
+///
+public class NamePlateSimpleParts(NamePlateStringField field)
+{
+ ///
+ /// Gets or sets the opening and closing SeStrings which will wrap the text, which can be used to apply colors or
+ /// styling to the field's text.
+ ///
+ public (SeString, SeString)? TextWrap { get; set; }
+
+ ///
+ /// Gets or sets this field's text.
+ ///
+ public SeString? Text { get; set; }
+
+ ///
+ /// Applies the changes from this builder to the actual field.
+ ///
+ /// The handler to perform the changes on.
+ internal unsafe void Apply(NamePlateUpdateHandler handler)
+ {
+ if ((nint)handler.GetFieldAsPointer(field) == NamePlateGui.EmptyStringPointer)
+ return;
+
+ if (this.TextWrap is { Item1: var left, Item2: var right })
+ {
+ var sb = new SeStringBuilder();
+ sb.Append(left);
+ sb.Append(this.Text ?? handler.GetFieldAsSeString(field));
+ sb.Append(right);
+ handler.SetField(field, sb.Build());
+ }
+ else if (this.Text is not null)
+ {
+ handler.SetField(field, this.Text);
+ }
+ }
+}
diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateStringField.cs b/Dalamud/Game/Gui/NamePlate/NamePlateStringField.cs
new file mode 100644
index 000000000..022935216
--- /dev/null
+++ b/Dalamud/Game/Gui/NamePlate/NamePlateStringField.cs
@@ -0,0 +1,38 @@
+namespace Dalamud.Game.Gui.NamePlate;
+
+///
+/// An enum describing the string fields available in nameplate data. The and various flags
+/// determine which fields will actually be rendered.
+///
+public enum NamePlateStringField
+{
+ ///
+ /// The object's name.
+ ///
+ Name = 0,
+
+ ///
+ /// The object's title.
+ ///
+ Title = 50,
+
+ ///
+ /// The object's free company tag.
+ ///
+ FreeCompanyTag = 100,
+
+ ///
+ /// The object's status prefix.
+ ///
+ StatusPrefix = 150,
+
+ ///
+ /// The object's target suffix.
+ ///
+ TargetSuffix = 200,
+
+ ///
+ /// The object's level prefix.
+ ///
+ LevelPrefix = 250,
+}
diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateUpdateContext.cs b/Dalamud/Game/Gui/NamePlate/NamePlateUpdateContext.cs
new file mode 100644
index 000000000..b8a4a9bd8
--- /dev/null
+++ b/Dalamud/Game/Gui/NamePlate/NamePlateUpdateContext.cs
@@ -0,0 +1,152 @@
+using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+using Dalamud.Game.ClientState.Objects;
+
+using FFXIVClientStructs.FFXIV.Client.UI;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+
+namespace Dalamud.Game.Gui.NamePlate;
+
+///
+/// Contains information related to the pending nameplate data update. This is only valid for a single frame and should
+/// not be kept across frames.
+///
+public interface INamePlateUpdateContext
+{
+ ///
+ /// Gets the number of active nameplates. The actual number visible may be lower than this in cases where some
+ /// nameplates are hidden by default (based on in-game "Display Name Settings" and so on).
+ ///
+ int ActiveNamePlateCount { get; }
+
+ ///
+ /// Gets a value indicating whether the game is currently performing a full update of all active nameplates.
+ ///
+ bool IsFullUpdate { get; }
+
+ ///
+ /// Gets the address of the NamePlate addon.
+ ///
+ nint AddonAddress { get; }
+
+ ///
+ /// Gets the address of the NamePlate addon's number array data container.
+ ///
+ nint NumberArrayDataAddress { get; }
+
+ ///
+ /// Gets the address of the NamePlate addon's string array data container.
+ ///
+ nint StringArrayDataAddress { get; }
+
+ ///
+ /// Gets the address of the first entry in the NamePlate addon's int array.
+ ///
+ nint NumberArrayDataEntryAddress { get; }
+}
+
+///
+/// Contains information related to the pending nameplate data update. This is only valid for a single frame and should
+/// not be kept across frames.
+///
+internal unsafe class NamePlateUpdateContext : INamePlateUpdateContext
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// An object table.
+ /// The addon lifecycle arguments for the update request.
+ internal NamePlateUpdateContext(ObjectTable objectTable, AddonRequestedUpdateArgs args)
+ {
+ this.ObjectTable = objectTable;
+ this.RaptureAtkModule = FFXIVClientStructs.FFXIV.Client.UI.RaptureAtkModule.Instance();
+ this.Ui3DModule = UIModule.Instance()->GetUI3DModule();
+ this.ResetState(args);
+ }
+
+ ///
+ /// Gets the number of active nameplates. The actual number visible may be lower than this in cases where some
+ /// nameplates are hidden by default (based on in-game "Display Name Settings" and so on).
+ ///
+ public int ActiveNamePlateCount { get; private set; }
+
+ ///
+ /// Gets a value indicating whether the game is currently performing a full update of all active nameplates.
+ ///
+ public bool IsFullUpdate { get; private set; }
+
+ ///
+ /// Gets the address of the NamePlate addon.
+ ///
+ public nint AddonAddress => (nint)this.Addon;
+
+ ///
+ /// Gets the address of the NamePlate addon's number array data container.
+ ///
+ public nint NumberArrayDataAddress => (nint)this.NumberData;
+
+ ///
+ /// Gets the address of the NamePlate addon's string array data container.
+ ///
+ public nint StringArrayDataAddress => (nint)this.StringData;
+
+ ///
+ /// Gets the address of the first entry in the NamePlate addon's int array.
+ ///
+ public nint NumberArrayDataEntryAddress => (nint)this.NumberStruct;
+
+ ///
+ /// Gets the RaptureAtkModule.
+ ///
+ internal RaptureAtkModule* RaptureAtkModule { get; }
+
+ ///
+ /// Gets the Ui3DModule.
+ ///
+ internal UI3DModule* Ui3DModule { get; }
+
+ ///
+ /// Gets the ObjectTable.
+ ///
+ internal ObjectTable ObjectTable { get; }
+
+ ///
+ /// Gets a pointer to the NamePlate addon.
+ ///
+ internal AddonNamePlate* Addon { get; private set; }
+
+ ///
+ /// Gets a pointer to the NamePlate addon's number array data container.
+ ///
+ internal NumberArrayData* NumberData { get; private set; }
+
+ ///
+ /// Gets a pointer to the NamePlate addon's string array data container.
+ ///
+ internal StringArrayData* StringData { get; private set; }
+
+ ///
+ /// Gets a pointer to the NamePlate addon's number array entries as a struct.
+ ///
+ internal AddonNamePlate.NamePlateIntArrayData* NumberStruct { get; private set; }
+
+ ///
+ /// Gets or sets a value indicating whether any handler in the current context has instantiated a part builder.
+ ///
+ internal bool HasParts { get; set; }
+
+ ///
+ /// Resets the state of the context based on the provided addon lifecycle arguments.
+ ///
+ /// The addon lifecycle arguments for the update request.
+ internal void ResetState(AddonRequestedUpdateArgs args)
+ {
+ this.Addon = (AddonNamePlate*)args.Addon;
+ this.NumberData = ((NumberArrayData**)args.NumberArrayData)![NamePlateGui.NumberArrayIndex];
+ this.NumberStruct = (AddonNamePlate.NamePlateIntArrayData*)this.NumberData->IntArray;
+ this.StringData = ((StringArrayData**)args.StringArrayData)![NamePlateGui.StringArrayIndex];
+ this.HasParts = false;
+
+ this.ActiveNamePlateCount = this.NumberStruct->ActiveNamePlateCount;
+ this.IsFullUpdate = this.Addon->DoFullUpdate != 0;
+ }
+}
diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs b/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs
new file mode 100644
index 000000000..99429d932
--- /dev/null
+++ b/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs
@@ -0,0 +1,616 @@
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+
+using Dalamud.Game.ClientState.Objects.SubKinds;
+using Dalamud.Game.ClientState.Objects.Types;
+using Dalamud.Game.Text.SeStringHandling;
+
+using FFXIVClientStructs.FFXIV.Client.UI;
+using FFXIVClientStructs.Interop;
+
+namespace Dalamud.Game.Gui.NamePlate;
+
+///
+/// A class representing a single nameplate. Provides mechanisms to look up the game object associated with the
+/// nameplate and allows for modification of various backing fields in number and string array data, which in turn
+/// affect aspects of the nameplate's appearance when drawn. Instances of this class are only valid for a single frame
+/// and should not be kept across frames.
+///
+public interface INamePlateUpdateHandler
+{
+ ///
+ /// Gets the GameObjectId of the game object associated with this nameplate.
+ ///
+ ulong GameObjectId { get; }
+
+ ///
+ /// Gets the associated with this nameplate, if possible. Performs an object table scan
+ /// and caches the result if successful.
+ ///
+ IGameObject? GameObject { get; }
+
+ ///
+ /// Gets a read-only view of the nameplate info object data for a nameplate. Modifications to
+ /// fields do not affect fields in the returned view.
+ ///
+ INamePlateInfoView InfoView { get; }
+
+ ///
+ /// Gets the index for this nameplate data in the backing number and string array data. This is not the same as the
+ /// rendered or object index, which can be retrieved from .
+ ///
+ int ArrayIndex { get; }
+
+ ///
+ /// Gets the associated with this nameplate, if possible. Returns null if the nameplate
+ /// has an associated , but that object cannot be assigned to .
+ ///
+ IBattleChara? BattleChara { get; }
+
+ ///
+ /// Gets the associated with this nameplate, if possible. Returns null if the
+ /// nameplate has an associated , but that object cannot be assigned to
+ /// .
+ ///
+ IPlayerCharacter? PlayerCharacter { get; }
+
+ ///
+ /// Gets the address of the nameplate info struct.
+ ///
+ nint NamePlateInfoAddress { get; }
+
+ ///
+ /// Gets the address of the first entry associated with this nameplate in the NamePlate addon's int array.
+ ///
+ nint NamePlateObjectAddress { get; }
+
+ ///
+ /// Gets a value indicating what kind of nameplate this is, based on the kind of object it is associated with.
+ ///
+ NamePlateKind NamePlateKind { get; }
+
+ ///
+ /// Gets the update flags for this nameplate.
+ ///
+ int UpdateFlags { get; }
+
+ ///
+ /// Gets or sets the overall text color for this nameplate. If this value is changed, the appropriate update flag
+ /// will be set so that the game will reflect this change immediately.
+ ///
+ uint TextColor { get; set; }
+
+ ///
+ /// Gets or sets the overall text edge color for this nameplate. If this value is changed, the appropriate update
+ /// flag will be set so that the game will reflect this change immediately.
+ ///
+ uint EdgeColor { get; set; }
+
+ ///
+ /// Gets or sets the icon ID for the nameplate's marker icon, which is the large icon used to indicate quest
+ /// availability and so on. This value is read from and reset by the game every frame, not just when a nameplate
+ /// changes. Setting this to 0 disables the icon.
+ ///
+ int MarkerIconId { get; set; }
+
+ ///
+ /// Gets or sets the icon ID for the nameplate's name icon, which is the small icon shown to the left of the name.
+ /// Setting this to -1 disables the icon.
+ ///
+ int NameIconId { get; set; }
+
+ ///
+ /// Gets the nameplate index, which is the index used for rendering and looking up entries in the object array. For
+ /// number and string array data, is used.
+ ///
+ int NamePlateIndex { get; }
+
+ ///
+ /// Gets the draw flags for this nameplate.
+ ///
+ int DrawFlags { get; }
+
+ ///
+ /// Gets or sets the visibility flags for this nameplate.
+ ///
+ int VisibilityFlags { get; set; }
+
+ ///
+ /// Gets a value indicating whether this nameplate is undergoing a major update or not. This is usually true when a
+ /// nameplate has just appeared or something meaningful about the entity has changed (e.g. its job or status). This
+ /// flag is reset by the game during the update process (during requested update and before draw).
+ ///
+ bool IsUpdating { get; }
+
+ ///
+ /// Gets or sets a value indicating whether the title (when visible) will be displayed above the object's name (a
+ /// prefix title) instead of below the object's name (a suffix title).
+ ///
+ bool IsPrefixTitle { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the title should be displayed at all.
+ ///
+ bool DisplayTitle { get; set; }
+
+ ///
+ /// Gets or sets the name for this nameplate.
+ ///
+ SeString Name { get; set; }
+
+ ///
+ /// Gets a builder which can be used to help cooperatively build a new name for this nameplate even when other
+ /// plugins modifying the name are present. Specifically, this builder allows setting text and text-wrapping
+ /// payloads (e.g. for setting text color) separately.
+ ///
+ NamePlateSimpleParts NameParts { get; }
+
+ ///
+ /// Gets or sets the title for this nameplate.
+ ///
+ SeString Title { get; set; }
+
+ ///
+ /// Gets a builder which can be used to help cooperatively build a new title for this nameplate even when other
+ /// plugins modifying the title are present. Specifically, this builder allows setting text, text-wrapping
+ /// payloads (e.g. for setting text color), and opening and closing quote sequences separately.
+ ///
+ NamePlateQuotedParts TitleParts { get; }
+
+ ///
+ /// Gets or sets the free company tag for this nameplate.
+ ///
+ SeString FreeCompanyTag { get; set; }
+
+ ///
+ /// Gets a builder which can be used to help cooperatively build a new FC tag for this nameplate even when other
+ /// plugins modifying the FC tag are present. Specifically, this builder allows setting text, text-wrapping
+ /// payloads (e.g. for setting text color), and opening and closing quote sequences separately.
+ ///
+ NamePlateQuotedParts FreeCompanyTagParts { get; }
+
+ ///
+ /// Gets or sets the status prefix for this nameplate. This prefix is used by the game to add BitmapFontIcon-based
+ /// online status icons to player nameplates.
+ ///
+ SeString StatusPrefix { get; set; }
+
+ ///
+ /// Gets or sets the target suffix for this nameplate. This suffix is used by the game to add the squared-letter
+ /// target tags to the end of combat target nameplates.
+ ///
+ SeString TargetSuffix { get; set; }
+
+ ///
+ /// Gets or sets the level prefix for this nameplate. This "Lv60" style prefix is added to enemy and friendly battle
+ /// NPC nameplates to indicate the NPC level.
+ ///
+ SeString LevelPrefix { get; set; }
+
+ ///
+ /// Removes the contents of the name field for this nameplate. This differs from simply setting the field
+ /// to an empty string because it writes a special value to memory, and other setters (except SetField variants)
+ /// will refuse to overwrite this value. Therefore, fields removed this way are more likely to stay removed.
+ ///
+ void RemoveName();
+
+ ///
+ /// Removes the contents of the title field for this nameplate. This differs from simply setting the field
+ /// to an empty string because it writes a special value to memory, and other setters (except SetField variants)
+ /// will refuse to overwrite this value. Therefore, fields removed this way are more likely to stay removed.
+ ///
+ void RemoveTitle();
+
+ ///
+ /// Removes the contents of the FC tag field for this nameplate. This differs from simply setting the field
+ /// to an empty string because it writes a special value to memory, and other setters (except SetField variants)
+ /// will refuse to overwrite this value. Therefore, fields removed this way are more likely to stay removed.
+ ///
+ void RemoveFreeCompanyTag();
+
+ ///
+ /// Removes the contents of the status prefix field for this nameplate. This differs from simply setting the field
+ /// to an empty string because it writes a special value to memory, and other setters (except SetField variants)
+ /// will refuse to overwrite this value. Therefore, fields removed this way are more likely to stay removed.
+ ///
+ void RemoveStatusPrefix();
+
+ ///
+ /// Removes the contents of the target suffix field for this nameplate. This differs from simply setting the field
+ /// to an empty string because it writes a special value to memory, and other setters (except SetField variants)
+ /// will refuse to overwrite this value. Therefore, fields removed this way are more likely to stay removed.
+ ///
+ void RemoveTargetSuffix();
+
+ ///
+ /// Removes the contents of the level prefix field for this nameplate. This differs from simply setting the field
+ /// to an empty string because it writes a special value to memory, and other setters (except SetField variants)
+ /// will refuse to overwrite this value. Therefore, fields removed this way are more likely to stay removed.
+ ///
+ void RemoveLevelPrefix();
+
+ ///
+ /// Gets a pointer to the string array value in the provided field.
+ ///
+ /// The field to read from.
+ /// A pointer to a sequence of non-null bytes.
+ unsafe byte* GetFieldAsPointer(NamePlateStringField field);
+
+ ///
+ /// Gets a byte span containing the string array value in the provided field.
+ ///
+ /// The field to read from.
+ /// A ReadOnlySpan containing a sequence of non-null bytes.
+ ReadOnlySpan GetFieldAsSpan(NamePlateStringField field);
+
+ ///
+ /// Gets a UTF8 string copy of the string array value in the provided field.
+ ///
+ /// The field to read from.
+ /// A copy of the string array value as a string.
+ string GetFieldAsString(NamePlateStringField field);
+
+ ///
+ /// Gets a parsed SeString copy of the string array value in the provided field.
+ ///
+ /// The field to read from.
+ /// A copy of the string array value as a parsed SeString.
+ SeString GetFieldAsSeString(NamePlateStringField field);
+
+ ///
+ /// Sets the string array value for the provided field.
+ ///
+ /// The field to write to.
+ /// The string to write.
+ void SetField(NamePlateStringField field, string value);
+
+ ///
+ /// Sets the string array value for the provided field.
+ ///
+ /// The field to write to.
+ /// The SeString to write.
+ void SetField(NamePlateStringField field, SeString value);
+
+ ///
+ /// Sets the string array value for the provided field. The provided byte sequence must be null-terminated.
+ ///
+ /// The field to write to.
+ /// The ReadOnlySpan of bytes to write.
+ void SetField(NamePlateStringField field, ReadOnlySpan value);
+
+ ///
+ /// Sets the string array value for the provided field. The provided byte sequence must be null-terminated.
+ ///
+ /// The field to write to.
+ /// The pointer to a null-terminated sequence of bytes to write.
+ unsafe void SetField(NamePlateStringField field, byte* value);
+
+ ///
+ /// Sets the string array value for the provided field to a fixed pointer to an empty string in unmanaged memory.
+ /// Other methods may notice this fixed pointer and refuse to overwrite it, preserving the emptiness of the field.
+ ///
+ /// The field to write to.
+ void RemoveField(NamePlateStringField field);
+}
+
+///
+/// A class representing a single nameplate. Provides mechanisms to look up the game object associated with the
+/// nameplate and allows for modification of various backing fields in number and string array data, which in turn
+/// affect aspects of the nameplate's appearance when drawn. Instances of this class are only valid for a single frame
+/// and should not be kept across frames.
+///
+internal unsafe class NamePlateUpdateHandler : INamePlateUpdateHandler
+{
+ private readonly NamePlateUpdateContext context;
+
+ private ulong? gameObjectId;
+ private IGameObject? gameObject;
+ private NamePlateInfoView? infoView;
+ private NamePlatePartsContainer? partsContainer;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The current update context.
+ /// The index for this nameplate data in the backing number and string array data. This is
+ /// not the same as the rendered index, which can be retrieved from .
+ internal NamePlateUpdateHandler(NamePlateUpdateContext context, int arrayIndex)
+ {
+ this.context = context;
+ this.ArrayIndex = arrayIndex;
+ }
+
+ ///
+ public int ArrayIndex { get; }
+
+ ///
+ public ulong GameObjectId => this.gameObjectId ??= this.NamePlateInfo->ObjectId;
+
+ ///
+ public IGameObject? GameObject => this.gameObject ??= this.context.ObjectTable.CreateObjectReference(
+ (nint)this.context.Ui3DModule->NamePlateObjectInfoPointers[
+ this.ArrayIndex].Value->GameObject);
+
+ ///
+ public IBattleChara? BattleChara => this.GameObject as IBattleChara;
+
+ ///
+ public IPlayerCharacter? PlayerCharacter => this.GameObject as IPlayerCharacter;
+
+ ///
+ public INamePlateInfoView InfoView => this.infoView ??= new NamePlateInfoView(this.NamePlateInfo);
+
+ ///
+ public nint NamePlateInfoAddress => (nint)this.NamePlateInfo;
+
+ ///
+ public nint NamePlateObjectAddress => (nint)this.NamePlateObject;
+
+ ///
+ public NamePlateKind NamePlateKind => (NamePlateKind)this.ObjectData->NamePlateKind;
+
+ ///
+ public int UpdateFlags
+ {
+ get => this.ObjectData->UpdateFlags;
+ private set => this.ObjectData->UpdateFlags = value;
+ }
+
+ ///
+ public uint TextColor
+ {
+ get => this.ObjectData->NameTextColor;
+ set
+ {
+ if (value != this.TextColor) this.UpdateFlags |= 2;
+ this.ObjectData->NameTextColor = value;
+ }
+ }
+
+ ///
+ public uint EdgeColor
+ {
+ get => this.ObjectData->NameEdgeColor;
+ set
+ {
+ if (value != this.EdgeColor) this.UpdateFlags |= 2;
+ this.ObjectData->NameEdgeColor = value;
+ }
+ }
+
+ ///
+ public int MarkerIconId
+ {
+ get => this.ObjectData->MarkerIconId;
+ set => this.ObjectData->MarkerIconId = value;
+ }
+
+ ///
+ public int NameIconId
+ {
+ get => this.ObjectData->NameIconId;
+ set => this.ObjectData->NameIconId = value;
+ }
+
+ ///
+ public int NamePlateIndex => this.ObjectData->NamePlateObjectIndex;
+
+ ///
+ public int DrawFlags
+ {
+ get => this.ObjectData->DrawFlags;
+ private set => this.ObjectData->DrawFlags = value;
+ }
+
+ ///
+ public int VisibilityFlags
+ {
+ get => ObjectData->VisibilityFlags;
+ set => ObjectData->VisibilityFlags = value;
+ }
+
+ ///
+ public bool IsUpdating => (this.UpdateFlags & 1) != 0;
+
+ ///
+ public bool IsPrefixTitle
+ {
+ get => (this.DrawFlags & 1) != 0;
+ set => this.DrawFlags = value ? this.DrawFlags | 1 : this.DrawFlags & ~1;
+ }
+
+ ///
+ public bool DisplayTitle
+ {
+ get => (this.DrawFlags & 0x80) == 0;
+ set => this.DrawFlags = value ? this.DrawFlags & ~0x80 : this.DrawFlags | 0x80;
+ }
+
+ ///
+ public SeString Name
+ {
+ get => this.GetFieldAsSeString(NamePlateStringField.Name);
+ set => this.WeakSetField(NamePlateStringField.Name, value);
+ }
+
+ ///
+ public NamePlateSimpleParts NameParts => this.PartsContainer.Name;
+
+ ///
+ public SeString Title
+ {
+ get => this.GetFieldAsSeString(NamePlateStringField.Title);
+ set => this.WeakSetField(NamePlateStringField.Title, value);
+ }
+
+ ///
+ public NamePlateQuotedParts TitleParts => this.PartsContainer.Title;
+
+ ///
+ public SeString FreeCompanyTag
+ {
+ get => this.GetFieldAsSeString(NamePlateStringField.FreeCompanyTag);
+ set => this.WeakSetField(NamePlateStringField.FreeCompanyTag, value);
+ }
+
+ ///
+ public NamePlateQuotedParts FreeCompanyTagParts => this.PartsContainer.FreeCompanyTag;
+
+ ///
+ public SeString StatusPrefix
+ {
+ get => this.GetFieldAsSeString(NamePlateStringField.StatusPrefix);
+ set => this.WeakSetField(NamePlateStringField.StatusPrefix, value);
+ }
+
+ ///
+ public SeString TargetSuffix
+ {
+ get => this.GetFieldAsSeString(NamePlateStringField.TargetSuffix);
+ set => this.WeakSetField(NamePlateStringField.TargetSuffix, value);
+ }
+
+ ///
+ public SeString LevelPrefix
+ {
+ get => this.GetFieldAsSeString(NamePlateStringField.LevelPrefix);
+ set => this.WeakSetField(NamePlateStringField.LevelPrefix, value);
+ }
+
+ ///
+ /// Gets or (lazily) creates a part builder container for this nameplate.
+ ///
+ internal NamePlatePartsContainer PartsContainer =>
+ this.partsContainer ??= new NamePlatePartsContainer(this.context);
+
+ private RaptureAtkModule.NamePlateInfo* NamePlateInfo =>
+ this.context.RaptureAtkModule->NamePlateInfoEntries.GetPointer(this.NamePlateIndex);
+
+ private AddonNamePlate.NamePlateObject* NamePlateObject =>
+ &this.context.Addon->NamePlateObjectArray[this.NamePlateIndex];
+
+ private AddonNamePlate.NamePlateIntArrayData.NamePlateObjectIntArrayData* ObjectData =>
+ this.context.NumberStruct->ObjectData.GetPointer(this.ArrayIndex);
+
+ ///
+ public void RemoveName() => this.RemoveField(NamePlateStringField.Name);
+
+ ///
+ public void RemoveTitle() => this.RemoveField(NamePlateStringField.Title);
+
+ ///
+ public void RemoveFreeCompanyTag() => this.RemoveField(NamePlateStringField.FreeCompanyTag);
+
+ ///
+ public void RemoveStatusPrefix() => this.RemoveField(NamePlateStringField.StatusPrefix);
+
+ ///
+ public void RemoveTargetSuffix() => this.RemoveField(NamePlateStringField.TargetSuffix);
+
+ ///
+ public void RemoveLevelPrefix() => this.RemoveField(NamePlateStringField.LevelPrefix);
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public byte* GetFieldAsPointer(NamePlateStringField field)
+ {
+ return this.context.StringData->StringArray[this.ArrayIndex + (int)field];
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public ReadOnlySpan GetFieldAsSpan(NamePlateStringField field)
+ {
+ return MemoryMarshal.CreateReadOnlySpanFromNullTerminated(this.GetFieldAsPointer(field));
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public string GetFieldAsString(NamePlateStringField field)
+ {
+ return Encoding.UTF8.GetString(this.GetFieldAsSpan(field));
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public SeString GetFieldAsSeString(NamePlateStringField field)
+ {
+ return SeString.Parse(this.GetFieldAsSpan(field));
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void SetField(NamePlateStringField field, string value)
+ {
+ this.context.StringData->SetValue(this.ArrayIndex + (int)field, value, true, true, true);
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void SetField(NamePlateStringField field, SeString value)
+ {
+ this.context.StringData->SetValue(
+ this.ArrayIndex + (int)field,
+ value.EncodeWithNullTerminator(),
+ true,
+ true,
+ true);
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void SetField(NamePlateStringField field, ReadOnlySpan value)
+ {
+ this.context.StringData->SetValue(this.ArrayIndex + (int)field, value, true, true, true);
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void SetField(NamePlateStringField field, byte* value)
+ {
+ this.context.StringData->SetValue(this.ArrayIndex + (int)field, value, true, true, true);
+ }
+
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void RemoveField(NamePlateStringField field)
+ {
+ this.context.StringData->SetValue(
+ this.ArrayIndex + (int)field,
+ (byte*)NamePlateGui.EmptyStringPointer,
+ true,
+ false,
+ true);
+ }
+
+ ///
+ /// Resets the state of this handler for re-use in a new update.
+ ///
+ internal void ResetState()
+ {
+ this.gameObjectId = null;
+ this.gameObject = null;
+ this.infoView = null;
+ this.partsContainer = null;
+ }
+
+ ///
+ /// Sets the string array value for the provided field, unless it was already set to the special empty string
+ /// pointer used by the Remove methods.
+ ///
+ /// The field to write to.
+ /// The SeString to write.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void WeakSetField(NamePlateStringField field, SeString value)
+ {
+ if ((nint)this.GetFieldAsPointer(field) == NamePlateGui.EmptyStringPointer)
+ return;
+ this.context.StringData->SetValue(
+ this.ArrayIndex + (int)field,
+ value.EncodeWithNullTerminator(),
+ true,
+ true,
+ true);
+ }
+}
diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/NamePlateAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/NamePlateAgingStep.cs
new file mode 100644
index 000000000..5a03a6dc2
--- /dev/null
+++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/NamePlateAgingStep.cs
@@ -0,0 +1,123 @@
+using System.Collections.Generic;
+
+using Dalamud.Game.Gui.NamePlate;
+using Dalamud.Game.Text.SeStringHandling;
+using Dalamud.Game.Text.SeStringHandling.Payloads;
+
+using ImGuiNET;
+
+namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps;
+
+///
+/// Tests for nameplates.
+///
+internal class NamePlateAgingStep : IAgingStep
+{
+ private SubStep currentSubStep;
+ private Dictionary? updateCount;
+
+ private enum SubStep
+ {
+ Start,
+ Confirm,
+ }
+
+ ///
+ public string Name => "Test Nameplates";
+
+ ///
+ public SelfTestStepResult RunStep()
+ {
+ var namePlateGui = Service.Get();
+
+ switch (this.currentSubStep)
+ {
+ case SubStep.Start:
+ namePlateGui.OnNamePlateUpdate += this.OnNamePlateUpdate;
+ namePlateGui.OnDataUpdate += this.OnDataUpdate;
+ namePlateGui.RequestRedraw();
+ this.updateCount = new Dictionary();
+ this.currentSubStep++;
+ break;
+
+ case SubStep.Confirm:
+ ImGui.Text("Click to redraw all visible nameplates");
+ if (ImGui.Button("Request redraw"))
+ namePlateGui.RequestRedraw();
+
+ ImGui.TextUnformatted("Can you see marker icons above nameplates, and does\n" +
+ "the update count increase when using request redraw?");
+
+ if (ImGui.Button("Yes"))
+ {
+ this.CleanUp();
+ return SelfTestStepResult.Pass;
+ }
+
+ ImGui.SameLine();
+
+ if (ImGui.Button("No"))
+ {
+ this.CleanUp();
+ return SelfTestStepResult.Fail;
+ }
+
+ break;
+ }
+
+ return SelfTestStepResult.Waiting;
+ }
+
+ ///
+ public void CleanUp()
+ {
+ var namePlateGui = Service.Get();
+ namePlateGui.OnNamePlateUpdate -= this.OnNamePlateUpdate;
+ namePlateGui.OnDataUpdate -= this.OnDataUpdate;
+ namePlateGui.RequestRedraw();
+ this.updateCount = null;
+ this.currentSubStep = SubStep.Start;
+ }
+
+ private void OnDataUpdate(INamePlateUpdateContext context, IReadOnlyList handlers)
+ {
+ foreach (var handler in handlers)
+ {
+ // Force nameplates to be visible
+ handler.VisibilityFlags |= 1;
+
+ // Set marker icon based on nameplate kind, and flicker when updating
+ if (handler.IsUpdating || context.IsFullUpdate)
+ {
+ handler.MarkerIconId = 66181 + (int)handler.NamePlateKind;
+ }
+ else
+ {
+ handler.MarkerIconId = 66161 + (int)handler.NamePlateKind;
+ }
+ }
+ }
+
+ private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList handlers)
+ {
+ foreach (var handler in handlers)
+ {
+ // Append GameObject address to name
+ var gameObjectAddress = handler.GameObject?.Address ?? 0;
+
+ handler.Name = handler.Name.Append(new SeString(new UIForegroundPayload(9)))
+ .Append($" (0x{gameObjectAddress:X})")
+ .Append(new SeString(UIForegroundPayload.UIForegroundOff));
+
+ // Track update count and set it as title
+ var count = this.updateCount!.GetValueOrDefault(handler.GameObjectId);
+ this.updateCount[handler.GameObjectId] = count + 1;
+
+ handler.TitleParts.Text = $"Updates: {count}";
+ handler.TitleParts.TextWrap = (new SeString(new UIForegroundPayload(43)),
+ new SeString(UIForegroundPayload.UIForegroundOff));
+ handler.DisplayTitle = true;
+ handler.IsPrefixTitle = false;
+ }
+ }
+}
diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs
index e3172d5c2..51c9b35f6 100644
--- a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs
@@ -28,6 +28,7 @@ internal class SelfTestWindow : Window
new EnterTerritoryAgingStep(148, "Central Shroud"),
new ItemPayloadAgingStep(),
new ContextMenuAgingStep(),
+ new NamePlateAgingStep(),
new ActorTableAgingStep(),
new FateTableAgingStep(),
new AetheryteListAgingStep(),
@@ -82,6 +83,7 @@ public override void Draw()
if (ImGuiComponents.IconButton(FontAwesomeIcon.StepForward))
{
this.stepResults.Add((SelfTestStepResult.NotRan, null));
+ this.steps[this.currentStep].CleanUp();
this.currentStep++;
this.lastTestStart = DateTimeOffset.Now;
diff --git a/Dalamud/Plugin/Services/INamePlateGui.cs b/Dalamud/Plugin/Services/INamePlateGui.cs
new file mode 100644
index 000000000..713d9120b
--- /dev/null
+++ b/Dalamud/Plugin/Services/INamePlateGui.cs
@@ -0,0 +1,48 @@
+using System.Collections.Generic;
+
+using Dalamud.Game.Gui.NamePlate;
+
+namespace Dalamud.Plugin.Services;
+
+///
+/// Class used to modify the data used when rendering nameplates.
+///
+public interface INamePlateGui
+{
+ ///
+ /// The delegate used for receiving nameplate update events.
+ ///
+ /// An object containing information about the pending data update.
+ /// A list of handlers used for updating nameplate data.
+ public delegate void OnPlateUpdateDelegate(
+ INamePlateUpdateContext context, IReadOnlyList handlers);
+
+ ///
+ /// An event which fires when nameplate data is updated and at least one nameplate has important updates. The
+ /// subscriber is provided with a list of handlers for nameplates with important updates.
+ ///
+ ///
+ /// Fires after .
+ ///
+ event OnPlateUpdateDelegate? OnNamePlateUpdate;
+
+ ///
+ /// An event which fires when nameplate data is updated. The subscriber is provided with a list of handlers for all
+ /// nameplates.
+ ///
+ ///
+ /// This event is likely to fire every frame even when no nameplates are actually updated, so in most cases
+ /// is preferred. Fires before .
+ ///
+ event OnPlateUpdateDelegate? OnDataUpdate;
+
+ ///
+ /// Requests that all nameplates should be redrawn on the following frame.
+ ///
+ ///
+ /// This causes extra work for the game, and should not need to be called every frame. However, it is acceptable to
+ /// call frequently when needed (e.g. in response to a manual settings change by the user) or when necessary (e.g.
+ /// after a change of zone, party type, etc.).
+ ///
+ void RequestRedraw();
+}