Merge pull request #1915 from nebel/array-nameplate-api

Add NamePlateGui
This commit is contained in:
goat 2024-07-20 18:29:55 +02:00 committed by GitHub
commit 8ca473839a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1644 additions and 0 deletions

View file

@ -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;
/// <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);
}
/// <summary>
/// Strips the surrounding quotes from a free company tag. If the quotes are not present in the expected location,
/// no modifications will be made.
/// </summary>
/// <param name="text">A quoted free company tag.</param>
/// <returns>A span containing the free company tag without its surrounding quote characters.</returns>
internal static ReadOnlySpan<byte> StripFreeCompanyTagQuotes(ReadOnlySpan<byte> text)
{
if (text.Length > 4 && text.StartsWith(" «"u8) && text.EndsWith("»"u8))
{
return text[3..^2];
}
return text;
}
/// <summary>
/// Strips the surrounding quotes from a title. If the quotes are not present in the expected location, no
/// modifications will be made.
/// </summary>
/// <param name="text">A quoted title.</param>
/// <returns>A span containing the title without its surrounding quote characters.</returns>
internal static ReadOnlySpan<byte> StripTitleQuotes(ReadOnlySpan<byte> 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<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,105 @@
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. For this field,
/// the quote characters which appear on either side of the title are NOT included.
/// </summary>
SeString FreeCompanyTag { get; }
/// <summary>
/// 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.
/// </summary>
SeString QuotedFreeCompanyTag { get; }
/// <summary>
/// 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.
/// </summary>
SeString Title { get; }
/// <summary>
/// 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.
/// </summary>
SeString QuotedTitle { 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? quotedFreeCompanyTag;
private SeString? title;
private SeString? quotedTitle;
private SeString? levelText;
/// <inheritdoc/>
public SeString Name => this.name ??= SeString.Parse(info->Name);
/// <inheritdoc/>
public SeString FreeCompanyTag => this.freeCompanyTag ??=
SeString.Parse(NamePlateGui.StripFreeCompanyTagQuotes(info->FcName));
/// <inheritdoc/>
public SeString QuotedFreeCompanyTag => this.quotedFreeCompanyTag ??= SeString.Parse(info->FcName);
/// <inheritdoc/>
public SeString Title => this.title ??= SeString.Parse(info->Title);
/// <inheritdoc/>
public SeString QuotedTitle => this.quotedTitle ??= 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,104 @@
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>
/// <remarks>
/// 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
/// <see cref="NamePlateInfoView"/>.
/// </remarks>
public class NamePlateQuotedParts(NamePlateStringField field, bool isFreeCompany)
{
/// <summary>
/// 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.
/// </summary>
public (SeString, SeString)? OuterWrap { get; set; }
/// <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.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)));
}
}

View file

@ -0,0 +1,51 @@
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>
/// <remarks>
/// 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
/// <see cref="NamePlateInfoView"/>.
/// </remarks>
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,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;
/// <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.Ui3DModule = UIModule.Instance()->GetUI3DModule();
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 Ui3DModule.
/// </summary>
internal UI3DModule* Ui3DModule { 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,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;
/// <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. Setting this to 0 disables the icon.
/// </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.
/// Setting this to -1 disables the icon.
/// </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. The provided byte sequence must be null-terminated.
/// </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. The provided byte sequence must be null-terminated.
/// </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.CreateObjectReference(
(nint)this.context.Ui3DModule->NamePlateObjectInfoPointers[
this.ArrayIndex].Value->GameObject);
/// <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.EncodeWithNullTerminator(),
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.EncodeWithNullTerminator(),
true,
true,
true);
}
}

View file

@ -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;
/// <summary>
/// Tests for nameplates.
/// </summary>
internal class NamePlateAgingStep : IAgingStep
{
private SubStep currentSubStep;
private Dictionary<ulong, int>? updateCount;
private enum SubStep
{
Start,
Confirm,
}
/// <inheritdoc/>
public string Name => "Test Nameplates";
/// <inheritdoc/>
public SelfTestStepResult RunStep()
{
var namePlateGui = Service<NamePlateGui>.Get();
switch (this.currentSubStep)
{
case SubStep.Start:
namePlateGui.OnNamePlateUpdate += this.OnNamePlateUpdate;
namePlateGui.OnDataUpdate += this.OnDataUpdate;
namePlateGui.RequestRedraw();
this.updateCount = new Dictionary<ulong, int>();
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;
}
/// <inheritdoc/>
public void CleanUp()
{
var namePlateGui = Service<NamePlateGui>.Get();
namePlateGui.OnNamePlateUpdate -= this.OnNamePlateUpdate;
namePlateGui.OnDataUpdate -= this.OnDataUpdate;
namePlateGui.RequestRedraw();
this.updateCount = null;
this.currentSubStep = SubStep.Start;
}
private void OnDataUpdate(INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> 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<INamePlateUpdateHandler> 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;
}
}
}

View file

@ -28,6 +28,7 @@ internal class SelfTestWindow : Window
new EnterTerritoryAgingStep(148, "Central Shroud"), new EnterTerritoryAgingStep(148, "Central Shroud"),
new ItemPayloadAgingStep(), new ItemPayloadAgingStep(),
new ContextMenuAgingStep(), new ContextMenuAgingStep(),
new NamePlateAgingStep(),
new ActorTableAgingStep(), new ActorTableAgingStep(),
new FateTableAgingStep(), new FateTableAgingStep(),
new AetheryteListAgingStep(), new AetheryteListAgingStep(),
@ -82,6 +83,7 @@ internal class SelfTestWindow : Window
if (ImGuiComponents.IconButton(FontAwesomeIcon.StepForward)) if (ImGuiComponents.IconButton(FontAwesomeIcon.StepForward))
{ {
this.stepResults.Add((SelfTestStepResult.NotRan, null)); this.stepResults.Add((SelfTestStepResult.NotRan, null));
this.steps[this.currentStep].CleanUp();
this.currentStep++; this.currentStep++;
this.lastTestStart = DateTimeOffset.Now; this.lastTestStart = DateTimeOffset.Now;

View file

@ -0,0 +1,48 @@
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>
/// <remarks>
/// Fires after <see cref="OnDataUpdate"/>.
/// </remarks>
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.
/// </summary>
/// <remarks>
/// This event is likely to fire every frame even when no nameplates are actually updated, so in most cases
/// <see cref="OnNamePlateUpdate"/> is preferred. Fires before <see cref="OnNamePlateUpdate"/>.
/// </remarks>
event OnPlateUpdateDelegate? OnDataUpdate;
/// <summary>
/// Requests that all nameplates should be redrawn on the following frame.
/// </summary>
/// <remarks>
/// 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.).
/// </remarks>
void RequestRedraw();
}