Add NamePlateGui

This commit is contained in:
nebel 2024-07-10 02:38:03 +09:00
parent fd48e7be62
commit b2e30f7cc1
No known key found for this signature in database
10 changed files with 1407 additions and 0 deletions

View file

@ -0,0 +1,270 @@
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;
/// <summary>
/// Class used to modify the data used when rendering nameplates.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal sealed class NamePlateGui : IInternalDisposableService, INamePlateGui
{
/// <summary>
/// The index for the number array used by the NamePlate addon.
/// </summary>
public const int NumberArrayIndex = 5;
/// <summary>
/// The index for the string array used by the NamePlate addon.
/// </summary>
public const int StringArrayIndex = 4;
/// <summary>
/// The index for of the FullUpdate entry in the NamePlate number array.
/// </summary>
internal const int NumberArrayFullUpdateIndex = 4;
/// <summary>
/// An empty null-terminated string pointer allocated in unmanaged memory, used to tag removed fields.
/// </summary>
internal static readonly nint EmptyStringPointer = CreateEmptyStringPointer();
[ServiceManager.ServiceDependency]
private readonly AddonLifecycle addonLifecycle = Service<AddonLifecycle>.Get();
[ServiceManager.ServiceDependency]
private readonly GameGui gameGui = Service<GameGui>.Get();
[ServiceManager.ServiceDependency]
private readonly ObjectTable objectTable = Service<ObjectTable>.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);
}
/// <inheritdoc/>
public event INamePlateGui.OnPlateUpdateDelegate? OnNamePlateUpdate;
/// <inheritdoc/>
public event INamePlateGui.OnPlateUpdateDelegate? OnDataUpdate;
/// <inheritdoc/>
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);
}
}
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.addonLifecycle.UnregisterListener(this.preRequestedUpdateListener);
}
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<NamePlateUpdateHandler>();
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<NamePlateUpdateHandler>(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<NamePlateUpdateHandler> handlers)
{
foreach (var handler in handlers)
{
if (handler.PartsContainer is { } container)
{
container.ApplyBuilders(handler);
}
}
}
}
/// <summary>
/// Plugin-scoped version of a AddonEventManager service.
/// </summary>
[PluginInterface]
[ServiceManager.ScopedService]
#pragma warning disable SA1015
[ResolveVia<INamePlateGui>]
#pragma warning restore SA1015
internal class NamePlateGuiPluginScoped : IInternalDisposableService, INamePlateGui
{
[ServiceManager.ServiceDependency]
private readonly NamePlateGui parentService = Service<NamePlateGui>.Get();
/// <inheritdoc/>
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;
}
}
/// <inheritdoc/>
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;
/// <inheritdoc/>
public void RequestRedraw()
{
this.parentService.RequestRedraw();
}
/// <inheritdoc/>
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<INamePlateUpdateHandler> handlers)
{
this.OnNamePlateUpdateScoped?.Invoke(context, handlers);
}
private void OnDataUpdateForward(
INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
{
this.OnDataUpdateScoped?.Invoke(context, handlers);
}
}

View file

@ -0,0 +1,93 @@
using Dalamud.Game.Text.SeStringHandling;
using FFXIVClientStructs.FFXIV.Client.UI;
namespace Dalamud.Game.Gui.NamePlate;
/// <summary>
/// Provides a read-only view of the nameplate info object data for a nameplate. Modifications to
/// <see cref="NamePlateUpdateHandler"/> fields do not affect this data.
/// </summary>
public interface INamePlateInfoView
{
/// <summary>
/// Gets the displayed name for this nameplate according to the nameplate info object.
/// </summary>
SeString Name { get; }
/// <summary>
/// Gets the displayed free company tag for this nameplate according to the nameplate info object.
/// </summary>
SeString FreeCompanyTag { get; }
/// <summary>
/// Gets the displayed title for this nameplate according to the nameplate info object. In this field, the quote
/// characters which appear on either side of the title are NOT included.
/// </summary>
SeString Title { get; }
/// <summary>
/// Gets the displayed title for this nameplate according to the nameplate info object. In this field, the quote
/// characters which appear on either side of the title ARE included.
/// </summary>
SeString DisplayTitle { get; }
/// <summary>
/// Gets the displayed level text for this nameplate according to the nameplate info object.
/// </summary>
SeString LevelText { get; }
/// <summary>
/// Gets the flags for this nameplate according to the nameplate info object.
/// </summary>
int Flags { get; }
/// <summary>
/// Gets a value indicating whether this nameplate is considered 'dirty' or not according to the nameplate
/// info object.
/// </summary>
bool IsDirty { get; }
/// <summary>
/// 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 <see cref="Flags"/> field.
/// </summary>
bool IsPrefixTitle { get; }
}
/// <summary>
/// Provides a read-only view of the nameplate info object data for a nameplate. Modifications to
/// <see cref="NamePlateUpdateHandler"/> fields do not affect this data.
/// </summary>
internal unsafe class NamePlateInfoView(RaptureAtkModule.NamePlateInfo* info) : INamePlateInfoView
{
private SeString? name;
private SeString? freeCompanyTag;
private SeString? title;
private SeString? displayTitle;
private SeString? levelText;
/// <inheritdoc/>
public SeString Name => this.name ??= SeString.Parse(info->Name);
/// <inheritdoc/>
public SeString FreeCompanyTag => this.freeCompanyTag ??= SeString.Parse(info->FcName);
/// <inheritdoc/>
public SeString Title => this.title ??= SeString.Parse(info->Title);
/// <inheritdoc/>
public SeString DisplayTitle => this.displayTitle ??= SeString.Parse(info->DisplayTitle);
/// <inheritdoc/>
public SeString LevelText => this.levelText ??= SeString.Parse(info->LevelText);
/// <inheritdoc/>
public int Flags => info->Flags;
/// <inheritdoc/>
public bool IsDirty => info->IsDirty;
/// <inheritdoc/>
public bool IsPrefixTitle => ((info->Flags >> (8 * 3)) & 0xFF) == 1;
}

View file

@ -0,0 +1,57 @@
namespace Dalamud.Game.Gui.NamePlate;
/// <summary>
/// An enum describing what kind of game object this nameplate represents.
/// </summary>
public enum NamePlateKind : byte
{
/// <summary>
/// A player character.
/// </summary>
PlayerCharacter = 0,
/// <summary>
/// An event NPC or companion.
/// </summary>
EventNpcCompanion = 1,
/// <summary>
/// A retainer.
/// </summary>
Retainer = 2,
/// <summary>
/// An enemy battle NPC.
/// </summary>
BattleNpcEnemy = 3,
/// <summary>
/// A friendly battle NPC.
/// </summary>
BattleNpcFriendly = 4,
/// <summary>
/// An event object.
/// </summary>
EventObject = 5,
/// <summary>
/// Treasure.
/// </summary>
Treasure = 6,
/// <summary>
/// A gathering point.
/// </summary>
GatheringPoint = 7,
/// <summary>
/// A battle NPC with subkind 6.
/// </summary>
BattleNpcSubkind6 = 8,
/// <summary>
/// Something else.
/// </summary>
Other = 9,
}

View file

@ -0,0 +1,46 @@
namespace Dalamud.Game.Gui.NamePlate;
/// <summary>
/// A container for parts.
/// </summary>
internal class NamePlatePartsContainer
{
private NamePlateSimpleParts? nameParts;
private NamePlateQuotedParts? titleParts;
private NamePlateQuotedParts? freeCompanyTagParts;
/// <summary>
/// Initializes a new instance of the <see cref="NamePlatePartsContainer"/> class.
/// </summary>
/// <param name="context">The currently executing update context.</param>
public NamePlatePartsContainer(NamePlateUpdateContext context)
{
context.HasParts = true;
}
/// <summary>
/// Gets a parts object for constructing a nameplate name.
/// </summary>
internal NamePlateSimpleParts Name => this.nameParts ??= new NamePlateSimpleParts(NamePlateStringField.Name);
/// <summary>
/// Gets a parts object for constructing a nameplate title.
/// </summary>
internal NamePlateQuotedParts Title => this.titleParts ??= new NamePlateQuotedParts(NamePlateStringField.Title, false);
/// <summary>
/// Gets a parts object for constructing a nameplate free company tag.
/// </summary>
internal NamePlateQuotedParts FreeCompanyTag => this.freeCompanyTagParts ??= new NamePlateQuotedParts(NamePlateStringField.FreeCompanyTag, true);
/// <summary>
/// Applies all container parts.
/// </summary>
/// <param name="handler">The handler to apply the builders to.</param>
internal void ApplyBuilders(NamePlateUpdateHandler handler)
{
this.nameParts?.Apply(handler);
this.freeCompanyTagParts?.Apply(handler);
this.titleParts?.Apply(handler);
}
}

View file

@ -0,0 +1,73 @@
using Dalamud.Game.Text.SeStringHandling;
namespace Dalamud.Game.Gui.NamePlate;
/// <summary>
/// A part builder for constructing and setting quoted nameplate fields (i.e. free company tag and title).
/// </summary>
/// <param name="field">The field type which should be set.</param>
public class NamePlateQuotedParts(NamePlateStringField field, bool isFreeCompany)
{
/// <summary>
/// Gets or sets the opening quote string which appears before the text and opening text-wrap.
/// </summary>
public SeString? LeftQuote { get; set; }
/// <summary>
/// Gets or sets the closing quote string which appears after the text and closing text-wrap.
/// </summary>
public SeString? RightQuote { get; set; }
/// <summary>
/// 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.
/// </summary>
public (SeString, SeString)? TextWrap { get; set; }
/// <summary>
/// Gets or sets this field's text.
/// </summary>
public SeString? Text { get; set; }
/// <summary>
/// Applies the changes from this builder to the actual field.
/// </summary>
/// <param name="handler">The handler to perform the changes on.</param>
internal unsafe void Apply(NamePlateUpdateHandler handler)
{
if ((nint)handler.GetFieldAsPointer(field) == NamePlateGui.EmptyStringPointer)
return;
var sb = new SeStringBuilder();
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 ?? handler.GetFieldAsSeString(field));
sb.Append(right);
}
else
{
sb.Append(this.Text ?? handler.GetFieldAsSeString(field));
}
if (this.RightQuote is not null)
{
sb.Append(this.RightQuote);
}
else
{
sb.Append(isFreeCompany ? "»" : "》");
}
handler.SetField(field, sb.Build());
}
}

View file

@ -0,0 +1,44 @@
using Dalamud.Game.Text.SeStringHandling;
namespace Dalamud.Game.Gui.NamePlate;
/// <summary>
/// A part builder for constructing and setting a simple (unquoted) nameplate field.
/// </summary>
/// <param name="field">The field type which should be set.</param>
public class NamePlateSimpleParts(NamePlateStringField field)
{
/// <summary>
/// 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.
/// </summary>
public (SeString, SeString)? TextWrap { get; set; }
/// <summary>
/// Gets or sets this field's text.
/// </summary>
public SeString? Text { get; set; }
/// <summary>
/// Applies the changes from this builder to the actual field.
/// </summary>
/// <param name="handler">The handler to perform the changes on.</param>
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);
}
}
}

View file

@ -0,0 +1,38 @@
namespace Dalamud.Game.Gui.NamePlate;
/// <summary>
/// An enum describing the string fields available in nameplate data. The <see cref="NamePlateKind"/> and various flags
/// determine which fields will actually be rendered.
/// </summary>
public enum NamePlateStringField
{
/// <summary>
/// The object's name.
/// </summary>
Name = 0,
/// <summary>
/// The object's title.
/// </summary>
Title = 50,
/// <summary>
/// The object's free company tag.
/// </summary>
FreeCompanyTag = 100,
/// <summary>
/// The object's status prefix.
/// </summary>
StatusPrefix = 150,
/// <summary>
/// The object's target suffix.
/// </summary>
TargetSuffix = 200,
/// <summary>
/// The object's level prefix.
/// </summary>
LevelPrefix = 250,
}

View file

@ -0,0 +1,146 @@
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;
/// <summary>
/// Contains information related to the pending nameplate data update. This is only valid for a single frame and should
/// not be kept across frames.
/// </summary>
public interface INamePlateUpdateContext
{
/// <summary>
/// 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).
/// </summary>
int ActiveNamePlateCount { get; }
/// <summary>
/// Gets a value indicating whether the game is currently performing a full update of all active nameplates.
/// </summary>
bool IsFullUpdate { get; }
/// <summary>
/// Gets the address of the NamePlate addon.
/// </summary>
nint AddonAddress { get; }
/// <summary>
/// Gets the address of the NamePlate addon's number array data container.
/// </summary>
nint NumberArrayDataAddress { get; }
/// <summary>
/// Gets the address of the NamePlate addon's string array data container.
/// </summary>
nint StringArrayDataAddress { get; }
/// <summary>
/// Gets the address of the first entry in the NamePlate addon's int array.
/// </summary>
nint NumberArrayDataEntryAddress { get; }
}
/// <summary>
/// Contains information related to the pending nameplate data update. This is only valid for a single frame and should
/// not be kept across frames.
/// </summary>
internal unsafe class NamePlateUpdateContext : INamePlateUpdateContext
{
/// <summary>
/// Initializes a new instance of the <see cref="NamePlateUpdateContext"/> class.
/// </summary>
/// <param name="objectTable">An object table.</param>
/// <param name="args">The addon lifecycle arguments for the update request.</param>
internal NamePlateUpdateContext(ObjectTable objectTable, AddonRequestedUpdateArgs args)
{
this.ObjectTable = objectTable;
this.RaptureAtkModule = FFXIVClientStructs.FFXIV.Client.UI.RaptureAtkModule.Instance();
this.ResetState(args);
}
/// <summary>
/// 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).
/// </summary>
public int ActiveNamePlateCount { get; private set; }
/// <summary>
/// Gets a value indicating whether the game is currently performing a full update of all active nameplates.
/// </summary>
public bool IsFullUpdate { get; private set; }
/// <summary>
/// Gets the address of the NamePlate addon.
/// </summary>
public nint AddonAddress => (nint)this.Addon;
/// <summary>
/// Gets the address of the NamePlate addon's number array data container.
/// </summary>
public nint NumberArrayDataAddress => (nint)this.NumberData;
/// <summary>
/// Gets the address of the NamePlate addon's string array data container.
/// </summary>
public nint StringArrayDataAddress => (nint)this.StringData;
/// <summary>
/// Gets the address of the first entry in the NamePlate addon's int array.
/// </summary>
public nint NumberArrayDataEntryAddress => (nint)this.NumberStruct;
/// <summary>
/// Gets the RaptureAtkModule.
/// </summary>
internal RaptureAtkModule* RaptureAtkModule { get; }
/// <summary>
/// Gets the ObjectTable.
/// </summary>
internal ObjectTable ObjectTable { get; }
/// <summary>
/// Gets a pointer to the NamePlate addon.
/// </summary>
internal AddonNamePlate* Addon { get; private set; }
/// <summary>
/// Gets a pointer to the NamePlate addon's number array data container.
/// </summary>
internal NumberArrayData* NumberData { get; private set; }
/// <summary>
/// Gets a pointer to the NamePlate addon's string array data container.
/// </summary>
internal StringArrayData* StringData { get; private set; }
/// <summary>
/// Gets a pointer to the NamePlate addon's number array entries as a struct.
/// </summary>
internal AddonNamePlate.NamePlateIntArrayData* NumberStruct { get; private set; }
/// <summary>
/// Gets or sets a value indicating whether any handler in the current context has instantiated a part builder.
/// </summary>
internal bool HasParts { get; set; }
/// <summary>
/// Resets the state of the context based on the provided addon lifecycle arguments.
/// </summary>
/// <param name="args">The addon lifecycle arguments for the update request.</param>
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;
}
}

View file

@ -0,0 +1,603 @@
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;
/// <summary>
/// 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.
/// </summary>
public interface INamePlateUpdateHandler
{
/// <summary>
/// Gets the GameObjectId of the game object associated with this nameplate.
/// </summary>
ulong GameObjectId { get; }
/// <summary>
/// Gets the <see cref="IGameObject"/> associated with this nameplate, if possible. Performs an object table scan
/// and caches the result if successful.
/// </summary>
IGameObject? GameObject { get; }
/// <summary>
/// Gets a read-only view of the nameplate info object data for a nameplate. Modifications to
/// <see cref="NamePlateUpdateHandler"/> fields do not affect fields in the returned view.
/// </summary>
INamePlateInfoView InfoView { get; }
/// <summary>
/// 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 <see cref="NamePlateIndex"/>.
/// </summary>
int ArrayIndex { get; }
/// <summary>
/// Gets the <see cref="IBattleChara"/> associated with this nameplate, if possible. Returns null if the nameplate
/// has an associated <see cref="IGameObject"/>, but that object cannot be assigned to <see cref="IBattleChara"/>.
/// </summary>
IBattleChara? BattleChara { get; }
/// <summary>
/// Gets the <see cref="IPlayerCharacter"/> associated with this nameplate, if possible. Returns null if the
/// nameplate has an associated <see cref="IGameObject"/>, but that object cannot be assigned to
/// <see cref="IPlayerCharacter"/>.
/// </summary>
IPlayerCharacter? PlayerCharacter { get; }
/// <summary>
/// Gets the address of the nameplate info struct.
/// </summary>
nint NamePlateInfoAddress { get; }
/// <summary>
/// Gets the address of the first entry associated with this nameplate in the NamePlate addon's int array.
/// </summary>
nint NamePlateObjectAddress { get; }
/// <summary>
/// Gets a value indicating what kind of nameplate this is, based on the kind of object it is associated with.
/// </summary>
NamePlateKind NamePlateKind { get; }
/// <summary>
/// Gets the update flags for this nameplate.
/// </summary>
int UpdateFlags { get; }
/// <summary>
/// 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.
/// </summary>
uint TextColor { get; set; }
/// <summary>
/// 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.
/// </summary>
uint EdgeColor { get; set; }
/// <summary>
/// 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.
/// </summary>
int MarkerIconId { get; set; }
/// <summary>
/// Gets or sets the icon ID for the nameplate's name icon, which is the small icon shown to the left of the name.
/// </summary>
int NameIconId { get; set; }
/// <summary>
/// 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, <see cref="ArrayIndex"/> is used.
/// </summary>
int NamePlateIndex { get; }
/// <summary>
/// Gets the draw flags for this nameplate.
/// </summary>
int DrawFlags { get; }
/// <summary>
/// Gets or sets the visibility flags for this nameplate.
/// </summary>
int VisibilityFlags { get; set; }
/// <summary>
/// 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).
/// </summary>
bool IsUpdating { get; }
/// <summary>
/// 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).
/// </summary>
bool IsPrefixTitle { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the title should be displayed at all.
/// </summary>
bool DisplayTitle { get; set; }
/// <summary>
/// Gets or sets the name for this nameplate.
/// </summary>
SeString Name { get; set; }
/// <summary>
/// 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.
/// </summary>
NamePlateSimpleParts NameParts { get; }
/// <summary>
/// Gets or sets the title for this nameplate.
/// </summary>
SeString Title { get; set; }
/// <summary>
/// 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.
/// </summary>
NamePlateQuotedParts TitleParts { get; }
/// <summary>
/// Gets or sets the free company tag for this nameplate.
/// </summary>
SeString FreeCompanyTag { get; set; }
/// <summary>
/// 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.
/// </summary>
NamePlateQuotedParts FreeCompanyTagParts { get; }
/// <summary>
/// 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.
/// </summary>
SeString StatusPrefix { get; set; }
/// <summary>
/// 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.
/// </summary>
SeString TargetSuffix { get; set; }
/// <summary>
/// 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.
/// </summary>
SeString LevelPrefix { get; set; }
/// <summary>
/// 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.
/// </summary>
void RemoveName();
/// <summary>
/// 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.
/// </summary>
void RemoveTitle();
/// <summary>
/// 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.
/// </summary>
void RemoveFreeCompanyTag();
/// <summary>
/// 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.
/// </summary>
void RemoveStatusPrefix();
/// <summary>
/// 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.
/// </summary>
void RemoveTargetSuffix();
/// <summary>
/// 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.
/// </summary>
void RemoveLevelPrefix();
/// <summary>
/// Gets a pointer to the string array value in the provided field.
/// </summary>
/// <param name="field">The field to read from.</param>
/// <returns>A pointer to a sequence of non-null bytes.</returns>
unsafe byte* GetFieldAsPointer(NamePlateStringField field);
/// <summary>
/// Gets a byte span containing the string array value in the provided field.
/// </summary>
/// <param name="field">The field to read from.</param>
/// <returns>A ReadOnlySpan containing a sequence of non-null bytes.</returns>
ReadOnlySpan<byte> GetFieldAsSpan(NamePlateStringField field);
/// <summary>
/// Gets a UTF8 string copy of the string array value in the provided field.
/// </summary>
/// <param name="field">The field to read from.</param>
/// <returns>A copy of the string array value as a string.</returns>
string GetFieldAsString(NamePlateStringField field);
/// <summary>
/// Gets a parsed SeString copy of the string array value in the provided field.
/// </summary>
/// <param name="field">The field to read from.</param>
/// <returns>A copy of the string array value as a parsed SeString.</returns>
SeString GetFieldAsSeString(NamePlateStringField field);
/// <summary>
/// Sets the string array value for the provided field.
/// </summary>
/// <param name="field">The field to write to.</param>
/// <param name="value">The string to write.</param>
void SetField(NamePlateStringField field, string value);
/// <summary>
/// Sets the string array value for the provided field.
/// </summary>
/// <param name="field">The field to write to.</param>
/// <param name="value">The SeString to write.</param>
void SetField(NamePlateStringField field, SeString value);
/// <summary>
/// Sets the string array value for the provided field.
/// </summary>
/// <param name="field">The field to write to.</param>
/// <param name="value">The ReadOnlySpan of bytes to write.</param>
void SetField(NamePlateStringField field, ReadOnlySpan<byte> value);
/// <summary>
/// Sets the string array value for the provided field.
/// </summary>
/// <param name="field">The field to write to.</param>
/// <param name="value">The pointer to a null-terminated sequence of bytes to write.</param>
unsafe void SetField(NamePlateStringField field, byte* value);
/// <summary>
/// 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.
/// </summary>
/// <param name="field">The field to write to.</param>
void RemoveField(NamePlateStringField field);
}
/// <summary>
/// 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.
/// </summary>
internal unsafe class NamePlateUpdateHandler : INamePlateUpdateHandler
{
private readonly NamePlateUpdateContext context;
private ulong? gameObjectId;
private IGameObject? gameObject;
private NamePlateInfoView? infoView;
private NamePlatePartsContainer? partsContainer;
/// <summary>
/// Initializes a new instance of the <see cref="NamePlateUpdateHandler"/> class.
/// </summary>
/// <param name="context">The current update context.</param>
/// <param name="arrayIndex">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 <see cref="NamePlateIndex"/>.</param>
internal NamePlateUpdateHandler(NamePlateUpdateContext context, int arrayIndex)
{
this.context = context;
this.ArrayIndex = arrayIndex;
}
/// <inheritdoc/>
public int ArrayIndex { get; }
/// <inheritdoc/>
public ulong GameObjectId => this.gameObjectId ??= this.NamePlateInfo->ObjectId;
/// <inheritdoc/>
public IGameObject? GameObject => this.gameObject ??= this.context.ObjectTable.SearchById(this.GameObjectId);
/// <inheritdoc/>
public IBattleChara? BattleChara => this.GameObject as IBattleChara;
/// <inheritdoc/>
public IPlayerCharacter? PlayerCharacter => this.GameObject as IPlayerCharacter;
/// <inheritdoc/>
public INamePlateInfoView InfoView => this.infoView ??= new NamePlateInfoView(this.NamePlateInfo);
/// <inheritdoc/>
public nint NamePlateInfoAddress => (nint)this.NamePlateInfo;
/// <inheritdoc/>
public nint NamePlateObjectAddress => (nint)this.NamePlateObject;
/// <inheritdoc/>
public NamePlateKind NamePlateKind => (NamePlateKind)this.ObjectData->NamePlateKind;
/// <inheritdoc/>
public int UpdateFlags
{
get => this.ObjectData->UpdateFlags;
private set => this.ObjectData->UpdateFlags = value;
}
/// <inheritdoc/>
public uint TextColor
{
get => this.ObjectData->NameTextColor;
set
{
if (value != this.TextColor) this.UpdateFlags |= 2;
this.ObjectData->NameTextColor = value;
}
}
/// <inheritdoc/>
public uint EdgeColor
{
get => this.ObjectData->NameEdgeColor;
set
{
if (value != this.EdgeColor) this.UpdateFlags |= 2;
this.ObjectData->NameEdgeColor = value;
}
}
/// <inheritdoc/>
public int MarkerIconId
{
get => this.ObjectData->MarkerIconId;
set => this.ObjectData->MarkerIconId = value;
}
/// <inheritdoc/>
public int NameIconId
{
get => this.ObjectData->NameIconId;
set => this.ObjectData->NameIconId = value;
}
/// <inheritdoc/>
public int NamePlateIndex => this.ObjectData->NamePlateObjectIndex;
/// <inheritdoc/>
public int DrawFlags
{
get => this.ObjectData->DrawFlags;
private set => this.ObjectData->DrawFlags = value;
}
/// <inheritdoc/>
public int VisibilityFlags
{
get => ObjectData->VisibilityFlags;
set => ObjectData->VisibilityFlags = value;
}
/// <inheritdoc/>
public bool IsUpdating => (this.UpdateFlags & 1) != 0;
/// <inheritdoc/>
public bool IsPrefixTitle
{
get => (this.DrawFlags & 1) != 0;
set => this.DrawFlags = value ? this.DrawFlags | 1 : this.DrawFlags & ~1;
}
/// <inheritdoc/>
public bool DisplayTitle
{
get => (this.DrawFlags & 0x80) == 0;
set => this.DrawFlags = value ? this.DrawFlags & ~0x80 : this.DrawFlags | 0x80;
}
/// <inheritdoc/>
public SeString Name
{
get => this.GetFieldAsSeString(NamePlateStringField.Name);
set => this.WeakSetField(NamePlateStringField.Name, value);
}
/// <inheritdoc/>
public NamePlateSimpleParts NameParts => this.PartsContainer.Name;
/// <inheritdoc/>
public SeString Title
{
get => this.GetFieldAsSeString(NamePlateStringField.Title);
set => this.WeakSetField(NamePlateStringField.Title, value);
}
/// <inheritdoc/>
public NamePlateQuotedParts TitleParts => this.PartsContainer.Title;
/// <inheritdoc/>
public SeString FreeCompanyTag
{
get => this.GetFieldAsSeString(NamePlateStringField.FreeCompanyTag);
set => this.WeakSetField(NamePlateStringField.FreeCompanyTag, value);
}
/// <inheritdoc/>
public NamePlateQuotedParts FreeCompanyTagParts => this.PartsContainer.FreeCompanyTag;
/// <inheritdoc/>
public SeString StatusPrefix
{
get => this.GetFieldAsSeString(NamePlateStringField.StatusPrefix);
set => this.WeakSetField(NamePlateStringField.StatusPrefix, value);
}
/// <inheritdoc/>
public SeString TargetSuffix
{
get => this.GetFieldAsSeString(NamePlateStringField.TargetSuffix);
set => this.WeakSetField(NamePlateStringField.TargetSuffix, value);
}
/// <inheritdoc/>
public SeString LevelPrefix
{
get => this.GetFieldAsSeString(NamePlateStringField.LevelPrefix);
set => this.WeakSetField(NamePlateStringField.LevelPrefix, value);
}
/// <summary>
/// Gets or (lazily) creates a part builder container for this nameplate.
/// </summary>
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);
/// <inheritdoc/>
public void RemoveName() => this.RemoveField(NamePlateStringField.Name);
/// <inheritdoc/>
public void RemoveTitle() => this.RemoveField(NamePlateStringField.Title);
/// <inheritdoc/>
public void RemoveFreeCompanyTag() => this.RemoveField(NamePlateStringField.FreeCompanyTag);
/// <inheritdoc/>
public void RemoveStatusPrefix() => this.RemoveField(NamePlateStringField.StatusPrefix);
/// <inheritdoc/>
public void RemoveTargetSuffix() => this.RemoveField(NamePlateStringField.TargetSuffix);
/// <inheritdoc/>
public void RemoveLevelPrefix() => this.RemoveField(NamePlateStringField.LevelPrefix);
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public byte* GetFieldAsPointer(NamePlateStringField field)
{
return this.context.StringData->StringArray[this.ArrayIndex + (int)field];
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ReadOnlySpan<byte> GetFieldAsSpan(NamePlateStringField field)
{
return MemoryMarshal.CreateReadOnlySpanFromNullTerminated(this.GetFieldAsPointer(field));
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public string GetFieldAsString(NamePlateStringField field)
{
return Encoding.UTF8.GetString(this.GetFieldAsSpan(field));
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public SeString GetFieldAsSeString(NamePlateStringField field)
{
return SeString.Parse(this.GetFieldAsSpan(field));
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetField(NamePlateStringField field, string value)
{
this.context.StringData->SetValue(this.ArrayIndex + (int)field, value, true, true, true);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetField(NamePlateStringField field, SeString value)
{
this.context.StringData->SetValue(this.ArrayIndex + (int)field, value.Encode(), true, true, true);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetField(NamePlateStringField field, ReadOnlySpan<byte> value)
{
this.context.StringData->SetValue(this.ArrayIndex + (int)field, value, true, true, true);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetField(NamePlateStringField field, byte* value)
{
this.context.StringData->SetValue(this.ArrayIndex + (int)field, value, true, true, true);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RemoveField(NamePlateStringField field)
{
this.context.StringData->SetValue(
this.ArrayIndex + (int)field,
(byte*)NamePlateGui.EmptyStringPointer,
true,
false,
true);
}
/// <summary>
/// Resets the state of this handler for re-use in a new update.
/// </summary>
internal void ResetState()
{
this.gameObjectId = null;
this.gameObject = null;
this.infoView = null;
this.partsContainer = null;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="field">The field to write to.</param>
/// <param name="value">The SeString to write.</param>
[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.Encode(), true, true, true);
}
}

View file

@ -0,0 +1,37 @@
using System.Collections.Generic;
using Dalamud.Game.Gui.NamePlate;
namespace Dalamud.Plugin.Services;
/// <summary>
/// Class used to modify the data used when rendering nameplates.
/// </summary>
public interface INamePlateGui
{
/// <summary>
/// The delegate used for receiving nameplate update events.
/// </summary>
/// <param name="context">An object containing information about the pending data update.</param>
/// <param name="handlers>">A list of handlers used for updating nameplate data.</param>
public delegate void OnPlateUpdateDelegate(
INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers);
/// <summary>
/// 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.
/// </summary>
event OnPlateUpdateDelegate? OnNamePlateUpdate;
/// <summary>
/// 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 <see cref="OnNamePlateUpdate"/> is preferred.
/// </summary>
event OnPlateUpdateDelegate? OnDataUpdate;
/// <summary>
/// Requests that all nameplates should be redrawn on the following frame.
/// </summary>
void RequestRedraw();
}