Dalamud/Dalamud/Game/Gui/NamePlate/NamePlateUpdateHandler.cs

634 lines
24 KiB
C#

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.FFXIV.Client.UI.Arrays;
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
{
get
{
if (this.GameObjectId == 0xE0000000)
{
// Skipping Ui3DModule lookup for invalid nameplate (NamePlateInfo->ObjectId is 0xE0000000). This
// prevents crashes around certain Doman Reconstruction cutscenes.
return null;
}
var objectInfoPtr = this.context.Ui3DModule->NamePlateObjectInfoPointers[this.ArrayIndex];
if (objectInfoPtr.Value == null) return null;
var gameObjectPtr = objectInfoPtr.Value->GameObject;
if (gameObjectPtr == null) return null;
return this.gameObject ??= this.context.ObjectTable[gameObjectPtr->ObjectIndex];
}
}
/// <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 NamePlateNumberArray.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);
}
}