mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 10:17:22 +01:00
442 lines
14 KiB
C#
442 lines
14 KiB
C#
using System.Collections.Generic;
|
|
using System.Runtime.InteropServices;
|
|
|
|
using Dalamud.Game.ClientState.Objects;
|
|
using Dalamud.Hooking;
|
|
using Dalamud.IoC;
|
|
using Dalamud.IoC.Internal;
|
|
using Dalamud.Plugin.Services;
|
|
|
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
|
|
|
using Serilog;
|
|
|
|
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 GameGui gameGui = Service<GameGui>.Get();
|
|
|
|
[ServiceManager.ServiceDependency]
|
|
private readonly ObjectTable objectTable = Service<ObjectTable>.Get();
|
|
|
|
private readonly NamePlateGuiAddressResolver address;
|
|
|
|
private readonly Hook<AtkUnitBase.Delegates.OnRequestedUpdate> onRequestedUpdateHook;
|
|
|
|
private NamePlateUpdateContext? context;
|
|
|
|
private NamePlateUpdateHandler[] updateHandlers = [];
|
|
|
|
[ServiceManager.ServiceConstructor]
|
|
private unsafe NamePlateGui(TargetSigScanner sigScanner)
|
|
{
|
|
this.address = new NamePlateGuiAddressResolver();
|
|
this.address.Setup(sigScanner);
|
|
|
|
this.onRequestedUpdateHook = Hook<AtkUnitBase.Delegates.OnRequestedUpdate>.FromAddress(
|
|
this.address.OnRequestedUpdate,
|
|
this.OnRequestedUpdateDetour);
|
|
this.onRequestedUpdateHook.Enable();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public event INamePlateGui.OnPlateUpdateDelegate? OnNamePlateUpdate;
|
|
|
|
/// <inheritdoc/>
|
|
public event INamePlateGui.OnPlateUpdateDelegate? OnPostNamePlateUpdate;
|
|
|
|
/// <inheritdoc/>
|
|
public event INamePlateGui.OnPlateUpdateDelegate? OnDataUpdate;
|
|
|
|
/// <inheritdoc/>
|
|
public event INamePlateGui.OnPlateUpdateDelegate? OnPostDataUpdate;
|
|
|
|
/// <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.onRequestedUpdateHook.Dispose();
|
|
}
|
|
|
|
/// <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 unsafe void OnRequestedUpdateDetour(
|
|
AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
|
|
{
|
|
var calledOriginal = false;
|
|
|
|
try
|
|
{
|
|
if (this.OnDataUpdate == null && this.OnNamePlateUpdate == null && this.OnPostDataUpdate == null &&
|
|
this.OnPostNamePlateUpdate == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (this.context == null)
|
|
{
|
|
this.context = new NamePlateUpdateContext(this.objectTable);
|
|
this.CreateHandlers(this.context);
|
|
}
|
|
|
|
this.context.ResetState(addon, numberArrayData, stringArrayData);
|
|
|
|
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);
|
|
|
|
try
|
|
{
|
|
calledOriginal = true;
|
|
this.onRequestedUpdateHook.Original.Invoke(addon, numberArrayData, stringArrayData);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Log.Error(e, "Caught exception when calling original AddonNamePlate OnRequestedUpdate.");
|
|
}
|
|
|
|
this.OnPostNamePlateUpdate?.Invoke(this.context, activeHandlers);
|
|
this.OnPostDataUpdate?.Invoke(this.context, activeHandlers);
|
|
}
|
|
else
|
|
{
|
|
var updatedHandlers = new List<NamePlateUpdateHandler>(activeNamePlateCount);
|
|
foreach (var handler in activeHandlers)
|
|
{
|
|
handler.ResetState();
|
|
if (handler.IsUpdating)
|
|
updatedHandlers.Add(handler);
|
|
}
|
|
|
|
if (this.OnDataUpdate is not null)
|
|
{
|
|
this.OnDataUpdate?.Invoke(this.context, activeHandlers);
|
|
this.OnNamePlateUpdate?.Invoke(this.context, updatedHandlers);
|
|
|
|
if (this.context.HasParts)
|
|
this.ApplyBuilders(activeHandlers);
|
|
|
|
try
|
|
{
|
|
calledOriginal = true;
|
|
this.onRequestedUpdateHook.Original.Invoke(addon, numberArrayData, stringArrayData);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Log.Error(e, "Caught exception when calling original AddonNamePlate OnRequestedUpdate.");
|
|
}
|
|
|
|
this.OnPostNamePlateUpdate?.Invoke(this.context, updatedHandlers);
|
|
this.OnPostDataUpdate?.Invoke(this.context, activeHandlers);
|
|
}
|
|
else if (updatedHandlers.Count != 0)
|
|
{
|
|
this.OnNamePlateUpdate?.Invoke(this.context, updatedHandlers);
|
|
|
|
if (this.context.HasParts)
|
|
this.ApplyBuilders(updatedHandlers);
|
|
|
|
try
|
|
{
|
|
calledOriginal = true;
|
|
this.onRequestedUpdateHook.Original.Invoke(addon, numberArrayData, stringArrayData);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Log.Error(e, "Caught exception when calling original AddonNamePlate OnRequestedUpdate.");
|
|
}
|
|
|
|
this.OnPostNamePlateUpdate?.Invoke(this.context, updatedHandlers);
|
|
this.OnPostDataUpdate?.Invoke(this.context, activeHandlers);
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (!calledOriginal)
|
|
{
|
|
try
|
|
{
|
|
this.onRequestedUpdateHook.Original.Invoke(addon, numberArrayData, stringArrayData);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Log.Error(e, "Caught exception when calling original AddonNamePlate OnRequestedUpdate.");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ApplyBuilders(Span<NamePlateUpdateHandler> handlers)
|
|
{
|
|
foreach (var handler in handlers)
|
|
{
|
|
if (handler.PartsContainer is { } container)
|
|
{
|
|
container.ApplyBuilders(handler);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ApplyBuilders(List<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? OnPostNamePlateUpdate
|
|
{
|
|
add
|
|
{
|
|
if (this.OnPostNamePlateUpdateScoped == null)
|
|
this.parentService.OnPostNamePlateUpdate += this.OnPostNamePlateUpdateForward;
|
|
|
|
this.OnPostNamePlateUpdateScoped += value;
|
|
}
|
|
|
|
remove
|
|
{
|
|
this.OnPostNamePlateUpdateScoped -= value;
|
|
if (this.OnPostNamePlateUpdateScoped == null)
|
|
this.parentService.OnPostNamePlateUpdate -= this.OnPostNamePlateUpdateForward;
|
|
}
|
|
}
|
|
|
|
/// <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;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public event INamePlateGui.OnPlateUpdateDelegate? OnPostDataUpdate
|
|
{
|
|
add
|
|
{
|
|
if (this.OnPostDataUpdateScoped == null)
|
|
this.parentService.OnPostDataUpdate += this.OnPostDataUpdateForward;
|
|
|
|
this.OnPostDataUpdateScoped += value;
|
|
}
|
|
|
|
remove
|
|
{
|
|
this.OnPostDataUpdateScoped -= value;
|
|
if (this.OnPostDataUpdateScoped == null)
|
|
this.parentService.OnPostDataUpdate -= this.OnPostDataUpdateForward;
|
|
}
|
|
}
|
|
|
|
private event INamePlateGui.OnPlateUpdateDelegate? OnNamePlateUpdateScoped;
|
|
|
|
private event INamePlateGui.OnPlateUpdateDelegate? OnPostNamePlateUpdateScoped;
|
|
|
|
private event INamePlateGui.OnPlateUpdateDelegate? OnDataUpdateScoped;
|
|
|
|
private event INamePlateGui.OnPlateUpdateDelegate? OnPostDataUpdateScoped;
|
|
|
|
/// <inheritdoc/>
|
|
public void RequestRedraw()
|
|
{
|
|
this.parentService.RequestRedraw();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void DisposeService()
|
|
{
|
|
this.parentService.OnNamePlateUpdate -= this.OnNamePlateUpdateForward;
|
|
this.OnNamePlateUpdateScoped = null;
|
|
|
|
this.parentService.OnPostNamePlateUpdate -= this.OnPostNamePlateUpdateForward;
|
|
this.OnPostNamePlateUpdateScoped = null;
|
|
|
|
this.parentService.OnDataUpdate -= this.OnDataUpdateForward;
|
|
this.OnDataUpdateScoped = null;
|
|
|
|
this.parentService.OnPostDataUpdate -= this.OnPostDataUpdateForward;
|
|
this.OnPostDataUpdateScoped = null;
|
|
}
|
|
|
|
private void OnNamePlateUpdateForward(
|
|
INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
|
|
{
|
|
this.OnNamePlateUpdateScoped?.Invoke(context, handlers);
|
|
}
|
|
|
|
private void OnPostNamePlateUpdateForward(
|
|
INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
|
|
{
|
|
this.OnPostNamePlateUpdateScoped?.Invoke(context, handlers);
|
|
}
|
|
|
|
private void OnDataUpdateForward(
|
|
INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
|
|
{
|
|
this.OnDataUpdateScoped?.Invoke(context, handlers);
|
|
}
|
|
|
|
private void OnPostDataUpdateForward(
|
|
INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
|
|
{
|
|
this.OnPostDataUpdateScoped?.Invoke(context, handlers);
|
|
}
|
|
}
|