Files
LightlessClient/LightlessSync/Services/NameplateHandler.cs
2025-10-05 15:45:32 +02:00

311 lines
11 KiB
C#

using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LightlessSync.Services.Mediator;
using LightlessSync.UI;
using LightlessSync.Utils;
// Created using https://github.com/PunishedPineapple/Distance as a reference, thank you!
using Microsoft.Extensions.Logging;
namespace LightlessSync.Services;
public unsafe class NameplateHandler : IMediatorSubscriber
{
private readonly ILogger<NameplateHandler> _logger;
private readonly IAddonLifecycle _addonLifecycle;
private readonly IGameGui _gameGui;
private readonly DalamudUtilService _dalamudUtil;
private readonly LightlessMediator _mediator;
public LightlessMediator Mediator => _mediator;
private bool mEnabled = false;
private bool _needsLabelRefresh = false;
private AddonNamePlate* mpNameplateAddon = null;
private readonly AtkTextNode*[] mTextNodes = new AtkTextNode*[AddonNamePlate.NumNamePlateObjects];
internal const uint mNameplateNodeIDBase = 0x7D99D500;
private volatile HashSet<string> _activeBroadcastingCids = new();
public NameplateHandler(ILogger<NameplateHandler> logger, IAddonLifecycle addonLifecycle, IGameGui gameGui, DalamudUtilService dalamudUtil, LightlessMediator mediator)
{
_logger = logger;
_addonLifecycle = addonLifecycle;
_gameGui = gameGui;
_dalamudUtil = dalamudUtil;
_mediator = mediator;
}
internal void Init()
{
EnableNameplate();
_mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, OnTick);
}
internal void Uninit()
{
DisableNameplate();
DestroyNameplateNodes();
_mediator.Unsubscribe<PriorityFrameworkUpdateMessage>(this);
mpNameplateAddon = null;
}
internal void EnableNameplate()
{
if (!mEnabled)
{
try
{
_addonLifecycle.RegisterListener(AddonEvent.PostDraw, "NamePlate", NameplateDrawDetour);
mEnabled = true;
}
catch (Exception e)
{
_logger.LogError($"Unknown error while trying to enable nameplate distances:\n{e}");
DisableNameplate();
}
}
}
internal void DisableNameplate()
{
if (mEnabled)
{
try
{
_addonLifecycle.UnregisterListener(NameplateDrawDetour);
}
catch (Exception e)
{
_logger.LogError($"Unknown error while unregistering nameplate listener:\n{e}");
}
mEnabled = false;
HideAllNameplateNodes();
}
}
private void NameplateDrawDetour(AddonEvent type, AddonArgs args)
{
var pNameplateAddon = (AddonNamePlate*)args.Addon.Address;
if (mpNameplateAddon != pNameplateAddon)
{
for (int i = 0; i < mTextNodes.Length; ++i) mTextNodes[i] = null;
mpNameplateAddon = pNameplateAddon;
if (mpNameplateAddon != null) CreateNameplateNodes();
}
UpdateNameplateNodes();
}
private void CreateNameplateNodes()
{
for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i)
{
var nameplateObject = GetNameplateObject(i);
if (nameplateObject == null)
continue;
var pNameplateResNode = nameplateObject.Value.NameContainer;
var pNewNode = AtkNodeHelpers.CreateOrphanTextNode(mNameplateNodeIDBase + (uint)i, TextFlags.Edge | TextFlags.Glare);
if (pNewNode != null)
{
var pLastChild = pNameplateResNode->ChildNode;
while (pLastChild->PrevSiblingNode != null) pLastChild = pLastChild->PrevSiblingNode;
pNewNode->AtkResNode.NextSiblingNode = pLastChild;
pNewNode->AtkResNode.ParentNode = pNameplateResNode;
pLastChild->PrevSiblingNode = (AtkResNode*)pNewNode;
nameplateObject.Value.RootComponentNode->Component->UldManager.UpdateDrawNodeList();
pNewNode->AtkResNode.SetUseDepthBasedPriority(true);
mTextNodes[i] = pNewNode;
}
}
}
private void DestroyNameplateNodes()
{
var pCurrentNameplateAddon = (AddonNamePlate*)_gameGui.GetAddonByName("NamePlate", 1).Address;
if (mpNameplateAddon == null || mpNameplateAddon != pCurrentNameplateAddon)
return;
for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i)
{
var pTextNode = mTextNodes[i];
var pNameplateNode = GetNameplateComponentNode(i);
if (pTextNode != null && pNameplateNode != null)
{
try
{
if (pTextNode->AtkResNode.PrevSiblingNode != null)
pTextNode->AtkResNode.PrevSiblingNode->NextSiblingNode = pTextNode->AtkResNode.NextSiblingNode;
if (pTextNode->AtkResNode.NextSiblingNode != null)
pTextNode->AtkResNode.NextSiblingNode->PrevSiblingNode = pTextNode->AtkResNode.PrevSiblingNode;
pNameplateNode->Component->UldManager.UpdateDrawNodeList();
pTextNode->AtkResNode.Destroy(true);
mTextNodes[i] = null;
}
catch (Exception e)
{
_logger.LogError($"Unknown error while removing text node 0x{(IntPtr)pTextNode:X} for nameplate {i} on component node 0x{(IntPtr)pNameplateNode:X}:\n{e}");
}
}
}
}
private void HideAllNameplateNodes()
{
for (int i = 0; i < mTextNodes.Length; ++i)
{
HideNameplateTextNode(i);
}
}
private void UpdateNameplateNodes()
{
var framework = Framework.Instance();
var ui3DModule = framework->GetUIModule()->GetUI3DModule();
if (ui3DModule == null)
return;
for (int i = 0; i < ui3DModule->NamePlateObjectInfoCount; ++i)
{
var objectInfo = ui3DModule->NamePlateObjectInfoPointers[i].Value;
if (objectInfo != null &&
objectInfo->GameObject != null &&
objectInfo->NamePlateIndex >= 0)
{
if (objectInfo == null || objectInfo->GameObject == null)
continue;
var nameplateIndex = objectInfo->NamePlateIndex;
if (nameplateIndex < 0 || nameplateIndex >= AddonNamePlate.NumNamePlateObjects)
continue;
var pNode = mTextNodes[nameplateIndex];
if (pNode == null)
continue;
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)objectInfo->GameObject);
//_logger.LogInformation($"checking cid: {cid}", cid);
if (cid == null || !_activeBroadcastingCids.Contains(cid))
{
pNode->AtkResNode.ToggleVisibility(false);
continue;
}
var nameplateObject = mpNameplateAddon->NamePlateObjectArray[nameplateIndex];
nameplateObject.RootComponentNode->Component->UldManager.UpdateDrawNodeList();
var pNameplateIconNode = nameplateObject.MarkerIcon;
var pNameplateResNode = nameplateObject.NameContainer;
var pNameplateTextNode = nameplateObject.NameText;
bool IsVisible = pNameplateIconNode->AtkResNode.IsVisible() || (pNameplateResNode->IsVisible() && pNameplateTextNode->AtkResNode.IsVisible());
pNode->AtkResNode.ToggleVisibility(IsVisible);
var nameContainer = nameplateObject.NameContainer;
var nameText = nameplateObject.NameText;
var labelColor = UIColors.Get("LightlessPurple");
var edgeColor = UIColors.Get("FullBlack");
var labelY = nameContainer->Height - nameplateObject.TextH - (int)(24 * nameText->AtkResNode.ScaleY);
pNode->AtkResNode.SetPositionShort(58, (short)labelY);
pNode->AtkResNode.SetUseDepthBasedPriority(true);
pNode->AtkResNode.SetScale(0.5f, 0.5f);
pNode->AtkResNode.Color.A = 255;
pNode->TextColor.R = (byte)(labelColor.X * 255);
pNode->TextColor.G = (byte)(labelColor.Y * 255);
pNode->TextColor.B = (byte)(labelColor.Z * 255);
pNode->TextColor.A = (byte)(labelColor.W * 255);
pNode->EdgeColor.R = (byte)(edgeColor.X * 255);
pNode->EdgeColor.G = (byte)(edgeColor.Y * 255);
pNode->EdgeColor.B = (byte)(edgeColor.Z * 255);
pNode->EdgeColor.A = (byte)(edgeColor.W * 255);
pNode->FontSize = 24;
pNode->AlignmentType = AlignmentType.Center;
pNode->FontType = FontType.MiedingerMed;
pNode->LineSpacing = 24;
pNode->CharSpacing = 1;
pNode->TextFlags = TextFlags.Edge | TextFlags.Glare;
pNode->SetText("Lightfinder");
}
}
}
private void HideNameplateTextNode(int i)
{
var pNode = mTextNodes[i];
if (pNode != null)
{
pNode->AtkResNode.ToggleVisibility(false);
}
}
private AddonNamePlate.NamePlateObject? GetNameplateObject(int i)
{
if (i < AddonNamePlate.NumNamePlateObjects &&
mpNameplateAddon != null &&
mpNameplateAddon->NamePlateObjectArray[i].RootComponentNode != null)
{
return mpNameplateAddon->NamePlateObjectArray[i];
}
else
{
return null;
}
}
private AtkComponentNode* GetNameplateComponentNode(int i)
{
var nameplateObject = GetNameplateObject(i);
return nameplateObject != null ? nameplateObject.Value.RootComponentNode : null;
}
public void FlagRefresh()
{
_needsLabelRefresh = true;
}
public void OnTick(PriorityFrameworkUpdateMessage _)
{
if (_needsLabelRefresh)
{
UpdateNameplateNodes();
_needsLabelRefresh = false;
}
}
public void UpdateBroadcastingCids(IEnumerable<string> cids)
{
var newSet = cids.ToHashSet();
var changed = !_activeBroadcastingCids.SetEquals(newSet);
if (!changed)
return;
_activeBroadcastingCids.Clear();
foreach (var cid in newSet)
_activeBroadcastingCids.Add(cid);
_logger.LogInformation("Active broadcast CIDs: {Cids}", string.Join(",", _activeBroadcastingCids));
FlagRefresh();
}
}