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; /// /// 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 GameGui gameGui = Service.Get(); [ServiceManager.ServiceDependency] private readonly ObjectTable objectTable = Service.Get(); private readonly NamePlateGuiAddressResolver address; private readonly Hook 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.FromAddress( this.address.OnRequestedUpdate, this.OnRequestedUpdateDetour); this.onRequestedUpdateHook.Enable(); } /// public event INamePlateGui.OnPlateUpdateDelegate? OnNamePlateUpdate; /// public event INamePlateGui.OnPlateUpdateDelegate? OnPostNamePlateUpdate; /// public event INamePlateGui.OnPlateUpdateDelegate? OnDataUpdate; /// public event INamePlateGui.OnPlateUpdateDelegate? OnPostDataUpdate; /// 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.onRequestedUpdateHook.Dispose(); } /// /// 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 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(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 handlers) { foreach (var handler in handlers) { if (handler.PartsContainer is { } container) { container.ApplyBuilders(handler); } } } private void ApplyBuilders(List 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? 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; } } /// 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; } } /// 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; /// public void RequestRedraw() { this.parentService.RequestRedraw(); } /// 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 handlers) { this.OnNamePlateUpdateScoped?.Invoke(context, handlers); } private void OnPostNamePlateUpdateForward( INamePlateUpdateContext context, IReadOnlyList handlers) { this.OnPostNamePlateUpdateScoped?.Invoke(context, handlers); } private void OnDataUpdateForward( INamePlateUpdateContext context, IReadOnlyList handlers) { this.OnDataUpdateScoped?.Invoke(context, handlers); } private void OnPostDataUpdateForward( INamePlateUpdateContext context, IReadOnlyList handlers) { this.OnPostDataUpdateScoped?.Invoke(context, handlers); } }