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 _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 _activeBroadcastingCids = new(); public NameplateHandler(ILogger 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(this, OnTick); } internal void Uninit() { DisableNameplate(); DestroyNameplateNodes(); _mediator.Unsubscribe(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) 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; } pNode->AtkResNode.ToggleVisibility(true); var nameplateObject = mpNameplateAddon->NamePlateObjectArray[nameplateIndex]; nameplateObject.RootComponentNode->Component->UldManager.UpdateDrawNodeList(); 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 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(); } }