694 lines
26 KiB
C#
694 lines
26 KiB
C#
using Dalamud.Game.Addon.Lifecycle;
|
|
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
|
using Dalamud.Game.ClientState.Objects.Enums;
|
|
using Dalamud.Game.Text;
|
|
using Dalamud.Plugin.Services;
|
|
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
|
using LightlessSync.LightlessConfiguration;
|
|
using LightlessSync.Services.Mediator;
|
|
using LightlessSync.UI;
|
|
using LightlessSync.UI.Services;
|
|
using LightlessSync.Utils;
|
|
using LightlessSync.UtilsEnum.Enum;
|
|
|
|
// Created using https://github.com/PunishedPineapple/Distance as a reference, thank you!
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
using System.Collections.Immutable;
|
|
using System.Globalization;
|
|
|
|
namespace LightlessSync.Services;
|
|
|
|
public unsafe class NameplateHandler : IMediatorSubscriber
|
|
{
|
|
private readonly ILogger<NameplateHandler> _logger;
|
|
private readonly IAddonLifecycle _addonLifecycle;
|
|
private readonly IGameGui _gameGui;
|
|
private readonly IObjectTable _objectTable;
|
|
private readonly LightlessConfigService _configService;
|
|
private readonly PairUiService _pairUiService;
|
|
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];
|
|
private readonly int[] _cachedNameplateTextWidths = new int[AddonNamePlate.NumNamePlateObjects];
|
|
private readonly int[] _cachedNameplateTextHeights = new int[AddonNamePlate.NumNamePlateObjects];
|
|
private readonly int[] _cachedNameplateContainerHeights = new int[AddonNamePlate.NumNamePlateObjects];
|
|
private readonly int[] _cachedNameplateTextOffsets = new int[AddonNamePlate.NumNamePlateObjects];
|
|
|
|
internal const uint mNameplateNodeIDBase = 0x7D99D500;
|
|
private const string DefaultLabelText = "LightFinder";
|
|
private const SeIconChar DefaultIcon = SeIconChar.Hyadelyn;
|
|
private static readonly string DefaultIconGlyph = SeIconCharExtensions.ToIconString(DefaultIcon);
|
|
|
|
private ImmutableHashSet<string> _activeBroadcastingCids = [];
|
|
|
|
public NameplateHandler(ILogger<NameplateHandler> logger, IAddonLifecycle addonLifecycle, IGameGui gameGui, LightlessConfigService configService, LightlessMediator mediator, IObjectTable objectTable, PairUiService pairUiService)
|
|
{
|
|
_logger = logger;
|
|
_addonLifecycle = addonLifecycle;
|
|
_gameGui = gameGui;
|
|
_configService = configService;
|
|
_mediator = mediator;
|
|
_objectTable = objectTable;
|
|
_pairUiService = pairUiService;
|
|
|
|
System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue);
|
|
}
|
|
|
|
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)
|
|
{
|
|
if (args.Addon.Address == nint.Zero)
|
|
{
|
|
if (_logger.IsEnabled(LogLevel.Warning))
|
|
_logger.LogWarning("Nameplate draw detour received a null addon address, skipping update.");
|
|
return;
|
|
}
|
|
|
|
var pNameplateAddon = (AddonNamePlate*)args.Addon.Address;
|
|
|
|
if (_mpNameplateAddon != pNameplateAddon)
|
|
{
|
|
for (int i = 0; i < _mTextNodes.Length; ++i) _mTextNodes[i] = null;
|
|
System.Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length);
|
|
System.Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length);
|
|
System.Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length);
|
|
System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue);
|
|
_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 rootNode = nameplateObject.Value.RootComponentNode;
|
|
if (rootNode == null || rootNode->Component == null)
|
|
continue;
|
|
|
|
var pNameplateResNode = nameplateObject.Value.NameContainer;
|
|
if (pNameplateResNode == null)
|
|
continue;
|
|
if (pNameplateResNode->ChildNode == null)
|
|
continue;
|
|
|
|
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;
|
|
rootNode->Component->UldManager.UpdateDrawNodeList();
|
|
pNewNode->AtkResNode.SetUseDepthBasedPriority(true);
|
|
_mTextNodes[i] = pNewNode;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DestroyNameplateNodes()
|
|
{
|
|
var currentHandle = _gameGui.GetAddonByName("NamePlate", 1);
|
|
if (currentHandle.Address == nint.Zero)
|
|
{
|
|
if (_logger.IsEnabled(LogLevel.Warning))
|
|
_logger.LogWarning("Unable to destroy nameplate nodes because the NamePlate addon is not available.");
|
|
return;
|
|
}
|
|
|
|
var pCurrentNameplateAddon = (AddonNamePlate*)currentHandle.Address;
|
|
if (_mpNameplateAddon == null)
|
|
return;
|
|
|
|
if (_mpNameplateAddon != pCurrentNameplateAddon)
|
|
{
|
|
if (_logger.IsEnabled(LogLevel.Warning))
|
|
_logger.LogWarning("Skipping nameplate node destroy due to addon address mismatch (cached {Cached}, current {Current}).", (IntPtr)_mpNameplateAddon, (IntPtr)pCurrentNameplateAddon);
|
|
return;
|
|
}
|
|
|
|
for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i)
|
|
{
|
|
var pTextNode = _mTextNodes[i];
|
|
var pNameplateNode = GetNameplateComponentNode(i);
|
|
if (pTextNode != null && (pNameplateNode == null || pNameplateNode->Component == null))
|
|
{
|
|
if (_logger.IsEnabled(LogLevel.Debug))
|
|
_logger.LogDebug("Skipping destroy for nameplate {Index} because its component node is unavailable.", i);
|
|
continue;
|
|
}
|
|
|
|
if (pTextNode != null && pNameplateNode != null && pNameplateNode->Component != 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(free: true);
|
|
_mTextNodes[i] = null;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
if (_logger.IsEnabled(LogLevel.Error))
|
|
_logger.LogError("Unknown error while removing text node 0x{textNode} for nameplate {i} on component node 0x{nameplateNode}:\n{e}", (IntPtr)pTextNode, i, (IntPtr)pNameplateNode, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
System.Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length);
|
|
System.Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length);
|
|
System.Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length);
|
|
System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue);
|
|
}
|
|
|
|
private void HideAllNameplateNodes()
|
|
{
|
|
for (int i = 0; i < _mTextNodes.Length; ++i)
|
|
{
|
|
HideNameplateTextNode(i);
|
|
}
|
|
}
|
|
|
|
private void UpdateNameplateNodes()
|
|
{
|
|
var currentHandle = _gameGui.GetAddonByName("NamePlate");
|
|
if (currentHandle.Address == nint.Zero)
|
|
{
|
|
if (_logger.IsEnabled(LogLevel.Debug))
|
|
_logger.LogDebug("NamePlate addon unavailable during update, skipping label refresh.");
|
|
return;
|
|
}
|
|
|
|
var currentAddon = (AddonNamePlate*)currentHandle.Address;
|
|
if (_mpNameplateAddon == null || currentAddon == null || currentAddon != _mpNameplateAddon)
|
|
{
|
|
if (_mpNameplateAddon != null && _logger.IsEnabled(LogLevel.Debug))
|
|
_logger.LogDebug("Cached NamePlate addon pointer differs from current: waiting for new hook (cached {Cached}, current {Current}).", (IntPtr)_mpNameplateAddon, (IntPtr)currentAddon);
|
|
return;
|
|
}
|
|
|
|
var framework = Framework.Instance();
|
|
if (framework == null)
|
|
{
|
|
if (_logger.IsEnabled(LogLevel.Debug))
|
|
_logger.LogDebug("Framework instance unavailable during nameplate update, skipping.");
|
|
return;
|
|
}
|
|
|
|
var uiModule = framework->GetUIModule();
|
|
if (uiModule == null)
|
|
{
|
|
if (_logger.IsEnabled(LogLevel.Debug))
|
|
_logger.LogDebug("UI module unavailable during nameplate update, skipping.");
|
|
return;
|
|
}
|
|
|
|
var ui3DModule = uiModule->GetUI3DModule();
|
|
if (ui3DModule == null)
|
|
{
|
|
if (_logger.IsEnabled(LogLevel.Debug))
|
|
_logger.LogDebug("UI3D module unavailable during nameplate update, skipping.");
|
|
return;
|
|
}
|
|
|
|
var vec = ui3DModule->NamePlateObjectInfoPointers;
|
|
if (vec.IsEmpty)
|
|
return;
|
|
|
|
var visibleUserIdsSnapshot = VisibleUserIds;
|
|
|
|
var safeCount = System.Math.Min(
|
|
ui3DModule->NamePlateObjectInfoCount,
|
|
vec.Length
|
|
);
|
|
|
|
for (int i = 0; i < safeCount; ++i)
|
|
{
|
|
var config = _configService.Current;
|
|
|
|
var objectInfoPtr = vec[i];
|
|
if (objectInfoPtr == null)
|
|
continue;
|
|
|
|
var objectInfo = objectInfoPtr.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 gameObject = objectInfo->GameObject;
|
|
if ((ObjectKind)gameObject->ObjectKind != ObjectKind.Player)
|
|
{
|
|
pNode->AtkResNode.ToggleVisibility(enable: false);
|
|
continue;
|
|
}
|
|
|
|
// CID gating
|
|
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)gameObject);
|
|
if (cid == null || !_activeBroadcastingCids.Contains(cid))
|
|
{
|
|
pNode->AtkResNode.ToggleVisibility(enable: false);
|
|
continue;
|
|
}
|
|
|
|
var local = _objectTable.LocalPlayer;
|
|
if (!config.LightfinderLabelShowOwn && local != null &&
|
|
objectInfo->GameObject->GetGameObjectId() == local.GameObjectId)
|
|
{
|
|
pNode->AtkResNode.ToggleVisibility(enable: false);
|
|
continue;
|
|
}
|
|
|
|
var hidePaired = !config.LightfinderLabelShowPaired;
|
|
|
|
var goId = (ulong)gameObject->GetGameObjectId();
|
|
if (hidePaired && visibleUserIdsSnapshot.Contains(goId))
|
|
{
|
|
pNode->AtkResNode.ToggleVisibility(enable: false);
|
|
continue;
|
|
}
|
|
|
|
var nameplateObject = _mpNameplateAddon->NamePlateObjectArray[nameplateIndex];
|
|
var root = nameplateObject.RootComponentNode;
|
|
var nameContainer = nameplateObject.NameContainer;
|
|
var nameText = nameplateObject.NameText;
|
|
var marker = nameplateObject.MarkerIcon;
|
|
|
|
if (root == null || root->Component == null || nameContainer == null || nameText == null)
|
|
{
|
|
_logger.LogDebug("Nameplate {Index} missing required nodes during update, skipping.", nameplateIndex);
|
|
pNode->AtkResNode.ToggleVisibility(enable: false);
|
|
continue;
|
|
}
|
|
|
|
root->Component->UldManager.UpdateDrawNodeList();
|
|
|
|
bool isVisible =
|
|
((marker != null) && marker->AtkResNode.IsVisible()) ||
|
|
(nameContainer->IsVisible() && nameText->AtkResNode.IsVisible()) ||
|
|
config.LightfinderLabelShowHidden;
|
|
|
|
pNode->AtkResNode.ToggleVisibility(isVisible);
|
|
if (!isVisible)
|
|
continue;
|
|
|
|
var labelColor = UIColors.Get("Lightfinder");
|
|
var edgeColor = UIColors.Get("LightfinderEdge");
|
|
|
|
var scaleMultiplier = System.Math.Clamp(config.LightfinderLabelScale, 0.5f, 2.0f);
|
|
var baseScale = config.LightfinderLabelUseIcon ? 1.0f : 0.5f;
|
|
var effectiveScale = baseScale * scaleMultiplier;
|
|
var labelContent = config.LightfinderLabelUseIcon
|
|
? NormalizeIconGlyph(config.LightfinderLabelIconGlyph)
|
|
: DefaultLabelText;
|
|
|
|
pNode->FontType = config.LightfinderLabelUseIcon ? FontType.Axis : FontType.MiedingerMed;
|
|
pNode->AtkResNode.SetScale(effectiveScale, effectiveScale);
|
|
var nodeWidth = (int)pNode->AtkResNode.GetWidth();
|
|
if (nodeWidth <= 0)
|
|
nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
|
|
var nodeHeight = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale);
|
|
var baseFontSize = config.LightfinderLabelUseIcon ? 36f : 24f;
|
|
var computedFontSize = (int)System.Math.Round(baseFontSize * scaleMultiplier);
|
|
pNode->FontSize = (byte)System.Math.Clamp(computedFontSize, 1, 255);
|
|
AlignmentType alignment;
|
|
|
|
var textScaleY = nameText->AtkResNode.ScaleY;
|
|
if (textScaleY <= 0f)
|
|
textScaleY = 1f;
|
|
|
|
var blockHeight = System.Math.Abs((int)nameplateObject.TextH);
|
|
if (blockHeight > 0)
|
|
{
|
|
_cachedNameplateTextHeights[nameplateIndex] = blockHeight;
|
|
}
|
|
else
|
|
{
|
|
blockHeight = _cachedNameplateTextHeights[nameplateIndex];
|
|
}
|
|
|
|
if (blockHeight <= 0)
|
|
{
|
|
blockHeight = GetScaledTextHeight(nameText);
|
|
if (blockHeight <= 0)
|
|
blockHeight = nodeHeight;
|
|
|
|
_cachedNameplateTextHeights[nameplateIndex] = blockHeight;
|
|
}
|
|
|
|
var containerHeight = (int)nameContainer->Height;
|
|
if (containerHeight > 0)
|
|
{
|
|
_cachedNameplateContainerHeights[nameplateIndex] = containerHeight;
|
|
}
|
|
else
|
|
{
|
|
containerHeight = _cachedNameplateContainerHeights[nameplateIndex];
|
|
}
|
|
|
|
if (containerHeight <= 0)
|
|
{
|
|
containerHeight = blockHeight + (int)System.Math.Round(8 * textScaleY);
|
|
if (containerHeight <= blockHeight)
|
|
containerHeight = blockHeight + 1;
|
|
|
|
_cachedNameplateContainerHeights[nameplateIndex] = containerHeight;
|
|
}
|
|
|
|
var blockTop = containerHeight - blockHeight;
|
|
if (blockTop < 0)
|
|
blockTop = 0;
|
|
var verticalPadding = (int)System.Math.Round(4 * effectiveScale);
|
|
|
|
var positionY = blockTop - verticalPadding - nodeHeight;
|
|
|
|
var textWidth = System.Math.Abs((int)nameplateObject.TextW);
|
|
if (textWidth <= 0)
|
|
{
|
|
textWidth = GetScaledTextWidth(nameText);
|
|
if (textWidth <= 0)
|
|
textWidth = nodeWidth;
|
|
}
|
|
|
|
if (textWidth > 0)
|
|
{
|
|
_cachedNameplateTextWidths[nameplateIndex] = textWidth;
|
|
}
|
|
|
|
var textOffset = (int)System.Math.Round(nameText->AtkResNode.X);
|
|
var hasValidOffset = true;
|
|
|
|
if (System.Math.Abs((int)nameplateObject.TextW) > 0 || textOffset != 0)
|
|
{
|
|
_cachedNameplateTextOffsets[nameplateIndex] = textOffset;
|
|
}
|
|
else
|
|
{
|
|
hasValidOffset = false;
|
|
}
|
|
int positionX;
|
|
|
|
|
|
if (!config.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal)))
|
|
labelContent = DefaultLabelText;
|
|
|
|
pNode->FontType = config.LightfinderLabelUseIcon ? FontType.Axis : FontType.MiedingerMed;
|
|
|
|
pNode->SetText(labelContent);
|
|
|
|
if (!config.LightfinderLabelUseIcon)
|
|
{
|
|
pNode->TextFlags &= ~TextFlags.AutoAdjustNodeSize;
|
|
pNode->AtkResNode.Width = 0;
|
|
nodeWidth = (int)pNode->AtkResNode.GetWidth();
|
|
if (nodeWidth <= 0)
|
|
nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
|
|
pNode->AtkResNode.Width = (ushort)nodeWidth;
|
|
}
|
|
else
|
|
{
|
|
pNode->TextFlags |= TextFlags.AutoAdjustNodeSize;
|
|
pNode->AtkResNode.Width = 0;
|
|
nodeWidth = pNode->AtkResNode.GetWidth();
|
|
}
|
|
|
|
|
|
if (config.LightfinderAutoAlign && nameContainer != null && hasValidOffset)
|
|
{
|
|
var nameplateWidth = (int)nameContainer->Width;
|
|
|
|
int leftPos = nameplateWidth / 8;
|
|
int rightPos = nameplateWidth - nodeWidth - (nameplateWidth / 8);
|
|
int centrePos = (nameplateWidth - nodeWidth) / 2;
|
|
int staticMargin = 24;
|
|
int calcMargin = (int)(nameplateWidth * 0.08f);
|
|
|
|
switch (config.LabelAlignment)
|
|
{
|
|
case LabelAlignment.Left:
|
|
positionX = config.LightfinderLabelUseIcon ? leftPos + staticMargin : leftPos;
|
|
alignment = AlignmentType.BottomLeft;
|
|
break;
|
|
case LabelAlignment.Right:
|
|
positionX = config.LightfinderLabelUseIcon ? rightPos - staticMargin : nameplateWidth - nodeWidth + calcMargin;
|
|
alignment = AlignmentType.BottomRight;
|
|
break;
|
|
default:
|
|
positionX = config.LightfinderLabelUseIcon ? centrePos : centrePos + calcMargin;
|
|
alignment = AlignmentType.Bottom;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
positionX = 58 + config.LightfinderLabelOffsetX;
|
|
alignment = AlignmentType.Bottom;
|
|
}
|
|
|
|
positionY += config.LightfinderLabelOffsetY;
|
|
|
|
alignment = (AlignmentType)System.Math.Clamp((int)alignment, 0, 8);
|
|
pNode->AtkResNode.SetUseDepthBasedPriority(enable: true);
|
|
|
|
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);
|
|
|
|
|
|
if (!config.LightfinderLabelUseIcon)
|
|
{
|
|
pNode->AlignmentType = AlignmentType.Bottom;
|
|
}
|
|
else
|
|
{
|
|
pNode->AlignmentType = alignment;
|
|
}
|
|
pNode->AtkResNode.SetPositionShort(
|
|
(short)System.Math.Clamp(positionX, short.MinValue, short.MaxValue),
|
|
(short)System.Math.Clamp(positionY, short.MinValue, short.MaxValue)
|
|
);
|
|
var computedLineSpacing = (int)System.Math.Round(24 * scaleMultiplier);
|
|
pNode->LineSpacing = (byte)System.Math.Clamp(computedLineSpacing, 0, byte.MaxValue);
|
|
pNode->CharSpacing = 1;
|
|
pNode->TextFlags = config.LightfinderLabelUseIcon
|
|
? TextFlags.Edge | TextFlags.Glare | TextFlags.AutoAdjustNodeSize
|
|
: TextFlags.Edge | TextFlags.Glare;
|
|
}
|
|
}
|
|
|
|
private static unsafe int GetScaledTextHeight(AtkTextNode* node)
|
|
{
|
|
if (node == null)
|
|
return 0;
|
|
|
|
var resNode = &node->AtkResNode;
|
|
var rawHeight = (int)resNode->GetHeight();
|
|
if (rawHeight <= 0 && node->LineSpacing > 0)
|
|
rawHeight = node->LineSpacing;
|
|
if (rawHeight <= 0)
|
|
rawHeight = AtkNodeHelpers.DefaultTextNodeHeight;
|
|
|
|
var scale = resNode->ScaleY;
|
|
if (scale <= 0f)
|
|
scale = 1f;
|
|
|
|
var computed = (int)System.Math.Round(rawHeight * scale);
|
|
return System.Math.Max(1, computed);
|
|
}
|
|
|
|
private static unsafe int GetScaledTextWidth(AtkTextNode* node)
|
|
{
|
|
if (node == null)
|
|
return 0;
|
|
|
|
var resNode = &node->AtkResNode;
|
|
var rawWidth = (int)resNode->GetWidth();
|
|
if (rawWidth <= 0)
|
|
rawWidth = AtkNodeHelpers.DefaultTextNodeWidth;
|
|
|
|
var scale = resNode->ScaleX;
|
|
if (scale <= 0f)
|
|
scale = 1f;
|
|
|
|
var computed = (int)System.Math.Round(rawWidth * scale);
|
|
return System.Math.Max(1, computed);
|
|
}
|
|
|
|
internal static string NormalizeIconGlyph(string? rawInput)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(rawInput))
|
|
return DefaultIconGlyph;
|
|
|
|
var trimmed = rawInput.Trim();
|
|
|
|
if (Enum.TryParse<SeIconChar>(trimmed, true, out var iconEnum))
|
|
return SeIconCharExtensions.ToIconString(iconEnum);
|
|
|
|
var hexCandidate = trimmed.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
|
|
? trimmed[2..]
|
|
: trimmed;
|
|
|
|
if (ushort.TryParse(hexCandidate, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var hexValue))
|
|
return char.ConvertFromUtf32(hexValue);
|
|
|
|
var enumerator = trimmed.EnumerateRunes();
|
|
if (enumerator.MoveNext())
|
|
return enumerator.Current.ToString();
|
|
|
|
return DefaultIconGlyph;
|
|
}
|
|
|
|
internal static string ToIconEditorString(string? rawInput)
|
|
{
|
|
var normalized = NormalizeIconGlyph(rawInput);
|
|
var runeEnumerator = normalized.EnumerateRunes();
|
|
return runeEnumerator.MoveNext()
|
|
? runeEnumerator.Current.Value.ToString("X4", CultureInfo.InvariantCulture)
|
|
: DefaultIconGlyph;
|
|
}
|
|
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];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private AtkComponentNode* GetNameplateComponentNode(int i)
|
|
{
|
|
var nameplateObject = GetNameplateObject(i);
|
|
return nameplateObject != null ? nameplateObject.Value.RootComponentNode : null;
|
|
}
|
|
|
|
private HashSet<ulong> VisibleUserIds
|
|
=> [.. _pairUiService.GetSnapshot().PairsByUid.Values
|
|
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
|
|
.Select(u => (ulong)u.PlayerCharacterId)];
|
|
|
|
public void FlagRefresh()
|
|
{
|
|
_needsLabelRefresh = true;
|
|
}
|
|
|
|
public void OnTick(PriorityFrameworkUpdateMessage _)
|
|
{
|
|
if (_needsLabelRefresh)
|
|
{
|
|
UpdateNameplateNodes();
|
|
_needsLabelRefresh = false;
|
|
}
|
|
}
|
|
|
|
public void UpdateBroadcastingCids(IEnumerable<string> cids)
|
|
{
|
|
var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal);
|
|
if (ReferenceEquals(_activeBroadcastingCids, newSet) || _activeBroadcastingCids.SetEquals(newSet))
|
|
return;
|
|
|
|
_activeBroadcastingCids = newSet;
|
|
if (_logger.IsEnabled(LogLevel.Information))
|
|
_logger.LogInformation("Active broadcast CIDs: {Cids}", string.Join(',', _activeBroadcastingCids));
|
|
FlagRefresh();
|
|
}
|
|
|
|
public void ClearNameplateCaches()
|
|
{
|
|
System.Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length);
|
|
System.Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length);
|
|
System.Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length);
|
|
System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue);
|
|
}
|
|
}
|