This commit is contained in:
2025-10-06 03:47:24 +09:00
parent 39784a1fea
commit c35650438c
22 changed files with 1480 additions and 184 deletions

View File

@@ -1,15 +1,22 @@
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
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.LightlessConfiguration.Configurations;
using LightlessSync.Services.Mediator;
using LightlessSync.UI;
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.Globalization;
using System.Text;
namespace LightlessSync.Services;
@@ -19,6 +26,7 @@ public unsafe class NameplateHandler : IMediatorSubscriber
private readonly IAddonLifecycle _addonLifecycle;
private readonly IGameGui _gameGui;
private readonly DalamudUtilService _dalamudUtil;
private readonly LightlessConfigService _configService;
private readonly LightlessMediator _mediator;
public LightlessMediator Mediator => _mediator;
@@ -26,18 +34,28 @@ public unsafe class NameplateHandler : IMediatorSubscriber
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.LinkMarker;
private static readonly string DefaultIconGlyph = SeIconCharExtensions.ToIconString(DefaultIcon);
private volatile HashSet<string> _activeBroadcastingCids = new();
public NameplateHandler(ILogger<NameplateHandler> logger, IAddonLifecycle addonLifecycle, IGameGui gameGui, DalamudUtilService dalamudUtil, LightlessMediator mediator)
public NameplateHandler(ILogger<NameplateHandler> logger, IAddonLifecycle addonLifecycle, IGameGui gameGui, DalamudUtilService dalamudUtil, LightlessConfigService configService, LightlessMediator mediator)
{
_logger = logger;
_addonLifecycle = addonLifecycle;
_gameGui = gameGui;
_dalamudUtil = dalamudUtil;
_configService = configService;
_mediator = mediator;
System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue);
}
internal void Init()
@@ -96,6 +114,10 @@ public unsafe class NameplateHandler : IMediatorSubscriber
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();
}
@@ -156,6 +178,11 @@ public unsafe class NameplateHandler : IMediatorSubscriber
}
}
}
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()
@@ -206,14 +233,139 @@ public unsafe class NameplateHandler : IMediatorSubscriber
var nameContainer = nameplateObject.NameContainer;
var nameText = nameplateObject.NameText;
if (nameContainer == null || nameText == null)
{
pNode->AtkResNode.ToggleVisibility(false);
continue;
}
var labelColor = UIColors.Get("LightlessPurple");
var edgeColor = UIColors.Get("FullBlack");
var config = _configService.Current;
var labelY = nameContainer->Height - nameplateObject.TextH - (int)(24 * nameText->AtkResNode.ScaleY);
var scaleMultiplier = System.Math.Clamp(config.LightfinderLabelScale, 0.5f, 2.0f);
var baseScale = config.LightfinderLabelUseIcon ? 1.0f : 0.5f;
var effectiveScale = baseScale * scaleMultiplier;
var nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
var nodeHeight = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale);
pNode->AtkResNode.SetPositionShort(58, (short)labelY);
int positionX = 58;
AlignmentType alignment = AlignmentType.Bottom;
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)
{
_cachedNameplateTextWidths[nameplateIndex] = textWidth;
}
else
{
textWidth = _cachedNameplateTextWidths[nameplateIndex];
}
if (textWidth <= 0)
{
textWidth = GetScaledTextWidth(nameText);
if (textWidth <= 0)
textWidth = nodeWidth;
_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 if (_cachedNameplateTextOffsets[nameplateIndex] != int.MinValue)
{
textOffset = _cachedNameplateTextOffsets[nameplateIndex];
}
else
{
hasValidOffset = false;
}
if (config.LightfinderAutoAlign && nameContainer != null && hasValidOffset)
{
switch (config.LabelAlignment)
{
case LabelAlignment.Left:
positionX = textOffset;
alignment = AlignmentType.BottomLeft;
break;
case LabelAlignment.Right:
positionX = textOffset + textWidth - nodeWidth;
alignment = AlignmentType.BottomRight;
break;
default:
positionX = textOffset + textWidth / 2 - nodeWidth / 2;
alignment = AlignmentType.Bottom;
break;
}
}
else
{
alignment = AlignmentType.Bottom;
}
positionX += config.LightfinderLabelOffsetX;
positionY += config.LightfinderLabelOffsetY;
alignment = (AlignmentType)System.Math.Clamp((int)alignment, 0, 8);
pNode->AtkResNode.SetPositionShort((short)System.Math.Clamp(positionX, short.MinValue, short.MaxValue), (short)System.Math.Clamp(positionY, short.MinValue, short.MaxValue));
pNode->AtkResNode.SetUseDepthBasedPriority(true);
pNode->AtkResNode.SetScale(0.5f, 0.5f);
pNode->AtkResNode.SetScale(effectiveScale, effectiveScale);
pNode->AtkResNode.Color.A = 255;
@@ -227,18 +379,97 @@ public unsafe class NameplateHandler : IMediatorSubscriber
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;
var baseFontSize = config.LightfinderLabelUseIcon ? 36f : 24f;
var computedFontSize = (int)System.Math.Round(baseFontSize * scaleMultiplier);
pNode->FontSize = (byte)System.Math.Clamp(computedFontSize, 1, 255);
pNode->AlignmentType = alignment;
var computedLineSpacing = (int)System.Math.Round(24 * scaleMultiplier);
pNode->LineSpacing = (byte)System.Math.Clamp(computedLineSpacing, 0, byte.MaxValue);
pNode->CharSpacing = 1;
pNode->TextFlags = TextFlags.Edge | TextFlags.Glare;
pNode->TextFlags = config.LightfinderLabelUseIcon
? TextFlags.Edge | TextFlags.Glare | TextFlags.AutoAdjustNodeSize
: TextFlags.Edge | TextFlags.Glare;
pNode->SetText("Lightfinder");
var labelContent = config.LightfinderLabelUseIcon
? NormalizeIconGlyph(config.LightfinderLabelIconGlyph)
: DefaultLabelText;
pNode->FontType = config.LightfinderLabelUseIcon ? FontType.Axis : FontType.MiedingerMed;
pNode->SetText(labelContent);
}
}
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];