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