Added documentation/comments, Added gpose detection.

This commit is contained in:
cake
2025-12-22 15:40:31 +01:00
parent fb4810980e
commit 9b4e48ad3e
2 changed files with 190 additions and 26 deletions

View File

@@ -267,6 +267,7 @@ public sealed class Plugin : IDalamudPlugin
sp.GetRequiredService<ILogger<LightFinderPlateHandler>>(), sp.GetRequiredService<ILogger<LightFinderPlateHandler>>(),
addonLifecycle, addonLifecycle,
gameGui, gameGui,
clientState,
sp.GetRequiredService<LightlessConfigService>(), sp.GetRequiredService<LightlessConfigService>(),
sp.GetRequiredService<LightlessMediator>(), sp.GetRequiredService<LightlessMediator>(),
objectTable, objectTable,

View File

@@ -28,12 +28,16 @@ using Task = System.Threading.Tasks.Task;
namespace LightlessSync.Services.LightFinder; namespace LightlessSync.Services.LightFinder;
/// <summary>
/// The new lightfinder nameplate handler using ImGUI (pictomancy) for rendering the icon/labels.
/// </summary>
public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscriber public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscriber
{ {
private readonly ILogger<LightFinderPlateHandler> _logger; private readonly ILogger<LightFinderPlateHandler> _logger;
private readonly IAddonLifecycle _addonLifecycle; private readonly IAddonLifecycle _addonLifecycle;
private readonly IGameGui _gameGui; private readonly IGameGui _gameGui;
private readonly IObjectTable _objectTable; private readonly IObjectTable _objectTable;
private readonly IClientState _clientState;
private readonly LightlessConfigService _configService; private readonly LightlessConfigService _configService;
private readonly PairUiService _pairUiService; private readonly PairUiService _pairUiService;
private readonly LightlessMediator _mediator; private readonly LightlessMediator _mediator;
@@ -44,16 +48,17 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
private bool _needsLabelRefresh; private bool _needsLabelRefresh;
private bool _drawSubscribed; private bool _drawSubscribed;
private AddonNamePlate* _mpNameplateAddon; private AddonNamePlate* _mpNameplateAddon;
private readonly object _labelLock = new(); private readonly Lock _labelLock = new();
private readonly NameplateBuffers _buffers = new(); private readonly NameplateBuffers _buffers = new();
private int _labelRenderCount; private int _labelRenderCount;
private const string DefaultLabelText = "LightFinder"; private const string _defaultLabelText = "LightFinder";
private const SeIconChar DefaultIcon = SeIconChar.Hyadelyn; private const SeIconChar _defaultIcon = SeIconChar.Hyadelyn;
private static readonly string _defaultIconGlyph = SeIconCharExtensions.ToIconString(DefaultIcon); private static readonly string _defaultIconGlyph = SeIconCharExtensions.ToIconString(_defaultIcon);
private static readonly Vector2 _defaultPivot = new(0.5f, 1f); private static readonly Vector2 _defaultPivot = new(0.5f, 1f);
private uint _lastNamePlateDrawFrame; private uint _lastNamePlateDrawFrame;
// / Overlay window flags
private const ImGuiWindowFlags _overlayFlags = private const ImGuiWindowFlags _overlayFlags =
ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoDecoration |
ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoBackground |
@@ -69,6 +74,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
ILogger<LightFinderPlateHandler> logger, ILogger<LightFinderPlateHandler> logger,
IAddonLifecycle addonLifecycle, IAddonLifecycle addonLifecycle,
IGameGui gameGui, IGameGui gameGui,
IClientState clientState,
LightlessConfigService configService, LightlessConfigService configService,
LightlessMediator mediator, LightlessMediator mediator,
IObjectTable objectTable, IObjectTable objectTable,
@@ -79,6 +85,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
_logger = logger; _logger = logger;
_addonLifecycle = addonLifecycle; _addonLifecycle = addonLifecycle;
_gameGui = gameGui; _gameGui = gameGui;
_clientState = clientState;
_configService = configService; _configService = configService;
_mediator = mediator; _mediator = mediator;
_objectTable = objectTable; _objectTable = objectTable;
@@ -113,6 +120,9 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
_mpNameplateAddon = null; _mpNameplateAddon = null;
} }
/// <summary>
/// Enable nameplate handling.
/// </summary>
internal void EnableNameplate() internal void EnableNameplate()
{ {
if (!_mEnabled) if (!_mEnabled)
@@ -130,6 +140,9 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
} }
} }
/// <summary>
/// Disable nameplate handling.
/// </summary>
internal void DisableNameplate() internal void DisableNameplate()
{ {
if (_mEnabled) if (_mEnabled)
@@ -148,8 +161,21 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
} }
} }
/// <summary>
/// Draw detour for nameplate addon.
/// </summary>
/// <param name="type"></param>
/// <param name="args"></param>
private void NameplateDrawDetour(AddonEvent type, AddonArgs args) private void NameplateDrawDetour(AddonEvent type, AddonArgs args)
{ {
if (_clientState.IsGPosing)
{
ClearLabelBuffer();
Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
_lastNamePlateDrawFrame = 0;
return;
}
if (args.Addon.Address == nint.Zero) if (args.Addon.Address == nint.Zero)
{ {
if (_logger.IsEnabled(LogLevel.Warning)) if (_logger.IsEnabled(LogLevel.Warning))
@@ -172,6 +198,9 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
UpdateNameplateNodes(); UpdateNameplateNodes();
} }
/// <summary>
/// Updates the nameplate nodes with LightFinder objects.
/// </summary>
private void UpdateNameplateNodes() private void UpdateNameplateNodes()
{ {
var currentHandle = _gameGui.GetAddonByName("NamePlate"); var currentHandle = _gameGui.GetAddonByName("NamePlate");
@@ -229,7 +258,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
} }
var visibleUserIdsSnapshot = VisibleUserIds; var visibleUserIdsSnapshot = VisibleUserIds;
var safeCount = System.Math.Min(ui3DModule->NamePlateObjectInfoCount, vec.Length); var safeCount = Math.Min(ui3DModule->NamePlateObjectInfoCount, vec.Length);
var currentConfig = _configService.Current; var currentConfig = _configService.Current;
var labelColor = UIColors.Get("Lightfinder"); var labelColor = UIColors.Get("Lightfinder");
var edgeColor = UIColors.Get("LightfinderEdge"); var edgeColor = UIColors.Get("LightfinderEdge");
@@ -273,7 +302,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
var root = nameplateObject.RootComponentNode; var root = nameplateObject.RootComponentNode;
var nameContainer = nameplateObject.NameContainer; var nameContainer = nameplateObject.NameContainer;
var nameText = nameplateObject.NameText; var nameText = nameplateObject.NameText;
var marker = nameplateObject.MarkerIcon;
if (root == null || root->Component == null || nameContainer == null || nameText == null) if (root == null || root->Component == null || nameContainer == null || nameText == null)
{ {
@@ -291,6 +319,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
if (!currentConfig.LightfinderLabelShowHidden && !isNameplateVisible) if (!currentConfig.LightfinderLabelShowHidden && !isNameplateVisible)
continue; continue;
// Prepare label content and scaling
var scaleMultiplier = System.Math.Clamp(currentConfig.LightfinderLabelScale, 0.5f, 2.0f); var scaleMultiplier = System.Math.Clamp(currentConfig.LightfinderLabelScale, 0.5f, 2.0f);
var baseScale = currentConfig.LightfinderLabelUseIcon ? 1.0f : 0.5f; var baseScale = currentConfig.LightfinderLabelUseIcon ? 1.0f : 0.5f;
var effectiveScale = baseScale * scaleMultiplier; var effectiveScale = baseScale * scaleMultiplier;
@@ -298,10 +327,10 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
var targetFontSize = (int)System.Math.Round(baseFontSize * scaleMultiplier); var targetFontSize = (int)System.Math.Round(baseFontSize * scaleMultiplier);
var labelContent = currentConfig.LightfinderLabelUseIcon var labelContent = currentConfig.LightfinderLabelUseIcon
? NormalizeIconGlyph(currentConfig.LightfinderLabelIconGlyph) ? NormalizeIconGlyph(currentConfig.LightfinderLabelIconGlyph)
: DefaultLabelText; : _defaultLabelText;
if (!currentConfig.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal))) if (!currentConfig.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal)))
labelContent = DefaultLabelText; labelContent = _defaultLabelText;
var nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale); var nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
var nodeHeight = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale); var nodeHeight = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale);
@@ -344,6 +373,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
() => GetScaledTextWidth(nameText), () => GetScaledTextWidth(nameText),
nodeWidth); nodeWidth);
// Text offset caching
var textOffset = (int)System.Math.Round(nameText->AtkResNode.X); var textOffset = (int)System.Math.Round(nameText->AtkResNode.X);
var hasValidOffset = TryCacheTextOffset(nameplateIndex, rawTextWidth, textOffset); var hasValidOffset = TryCacheTextOffset(nameplateIndex, rawTextWidth, textOffset);
@@ -354,11 +384,13 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
continue; continue;
} }
var res = nameContainer; var res = nameContainer;
// X scale
var worldScaleX = GetWorldScaleX(res); var worldScaleX = GetWorldScaleX(res);
if (worldScaleX <= 0f) worldScaleX = 1f; if (worldScaleX <= 0f) worldScaleX = 1f;
// Y scale
var worldScaleY = GetWorldScaleY(res); var worldScaleY = GetWorldScaleY(res);
if (worldScaleY <= 0f) worldScaleY = 1f; if (worldScaleY <= 0f) worldScaleY = 1f;
@@ -368,12 +400,15 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
float finalX; float finalX;
if (currentConfig.LightfinderAutoAlign) if (currentConfig.LightfinderAutoAlign)
{ {
// auto X positioning
var measuredWidth = Math.Max(1, textWidth > 0 ? textWidth : nodeWidth); var measuredWidth = Math.Max(1, textWidth > 0 ? textWidth : nodeWidth);
var measuredWidthF = (float)measuredWidth; var measuredWidthF = (float)measuredWidth;
// consider icon width
var containerWidthLocal = res->Width > 0 ? res->Width : measuredWidthF; var containerWidthLocal = res->Width > 0 ? res->Width : measuredWidthF;
var containerWidthScreen = containerWidthLocal * worldScaleX; var containerWidthScreen = containerWidthLocal * worldScaleX;
// container bounds for positions
var containerLeft = res->ScreenX; var containerLeft = res->ScreenX;
var containerRight = containerLeft + containerWidthScreen; var containerRight = containerLeft + containerWidthScreen;
var containerCenter = containerLeft + (containerWidthScreen * 0.5f); var containerCenter = containerLeft + (containerWidthScreen * 0.5f);
@@ -384,6 +419,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
var offsetXScreen = currentConfig.LightfinderLabelOffsetX * worldScaleX; var offsetXScreen = currentConfig.LightfinderLabelOffsetX * worldScaleX;
// alignment based on config
switch (currentConfig.LabelAlignment) switch (currentConfig.LabelAlignment)
{ {
case LabelAlignment.Left: case LabelAlignment.Left:
@@ -402,6 +438,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
} }
else else
{ {
// manual X positioning
var cachedTextOffset = _buffers.TextOffsets[nameplateIndex]; var cachedTextOffset = _buffers.TextOffsets[nameplateIndex];
var hasCachedOffset = cachedTextOffset != int.MinValue; var hasCachedOffset = cachedTextOffset != int.MinValue;
var baseOffsetXLocal = (!currentConfig.LightfinderLabelUseIcon && hasValidOffset && hasCachedOffset) var baseOffsetXLocal = (!currentConfig.LightfinderLabelUseIcon && hasValidOffset && hasCachedOffset)
@@ -419,6 +456,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
alignment = (AlignmentType)Math.Clamp((int)alignment, 0, 8); alignment = (AlignmentType)Math.Clamp((int)alignment, 0, 8);
// final position before smoothing
var finalPosition = new Vector2(finalX, res->ScreenY + positionYScreen); var finalPosition = new Vector2(finalX, res->ScreenY + positionYScreen);
var dpiScale = ImGui.GetIO().DisplayFramebufferScale.X; // often same for Y var dpiScale = ImGui.GetIO().DisplayFramebufferScale.X; // often same for Y
var fw = Framework.Instance(); var fw = Framework.Instance();
@@ -429,6 +467,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
finalPosition = SmoothPosition(nameplateIndex, finalPosition, dt); finalPosition = SmoothPosition(nameplateIndex, finalPosition, dt);
finalPosition = SnapToPixels(finalPosition, dpiScale); finalPosition = SnapToPixels(finalPosition, dpiScale);
// prepare label info
var pivot = (currentConfig.LightfinderAutoAlign || currentConfig.LightfinderLabelUseIcon) var pivot = (currentConfig.LightfinderAutoAlign || currentConfig.LightfinderLabelUseIcon)
? AlignmentToPivot(alignment) ? AlignmentToPivot(alignment)
: _defaultPivot; : _defaultPivot;
@@ -459,6 +498,9 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
} }
} }
/// <summary>
/// On each tick, process any needed updates for the UI Builder.
/// </summary>
private void OnUiBuilderDraw() private void OnUiBuilderDraw()
{ {
if (!_mEnabled) if (!_mEnabled)
@@ -468,6 +510,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
if (fw == null) if (fw == null)
return; return;
// Frame skip check
var frame = fw->FrameCounter; var frame = fw->FrameCounter;
if (_lastNamePlateDrawFrame == 0 || (frame - _lastNamePlateDrawFrame) > 1) if (_lastNamePlateDrawFrame == 0 || (frame - _lastNamePlateDrawFrame) > 1)
@@ -478,6 +521,16 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
return; return;
} }
//Gpose Check
if (_clientState.IsGPosing)
{
ClearLabelBuffer();
Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
_lastNamePlateDrawFrame = 0;
return;
}
// If nameplate addon is not visible, skip rendering
if (!IsNamePlateAddonVisible()) if (!IsNamePlateAddonVisible())
return; return;
@@ -506,6 +559,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
_uiRects.Clear(); _uiRects.Clear();
} }
// Needed for imgui overlay viewport for the multi window view.
var vp = ImGui.GetMainViewport(); var vp = ImGui.GetMainViewport();
var vpPos = vp.Pos; var vpPos = vp.Pos;
@@ -532,6 +586,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
{ {
ref var info = ref _buffers.LabelCopy[i]; ref var info = ref _buffers.LabelCopy[i];
// final draw position with viewport offset
var drawPos = info.ScreenPosition + vpPos; var drawPos = info.ScreenPosition + vpPos;
var font = default(ImFontPtr); var font = default(ImFontPtr);
if (info.UseIcon) if (info.UseIcon)
@@ -547,21 +602,25 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
if (!font.IsNull) if (!font.IsNull)
ImGui.PushFont(font); ImGui.PushFont(font);
// calculate size for occlusion checking
var baseSize = ImGui.CalcTextSize(info.Text); var baseSize = ImGui.CalcTextSize(info.Text);
var baseFontSize = ImGui.GetFontSize(); var baseFontSize = ImGui.GetFontSize();
if (!font.IsNull) if (!font.IsNull)
ImGui.PopFont(); ImGui.PopFont();
// scale size based on font size
var scale = baseFontSize > 0 ? (info.FontSize / baseFontSize) : 1f; var scale = baseFontSize > 0 ? (info.FontSize / baseFontSize) : 1f;
var size = baseSize * scale; var size = baseSize * scale;
// label rect for occlusion checking
var topLeft = info.ScreenPosition - new Vector2(size.X * info.Pivot.X, size.Y * info.Pivot.Y); var topLeft = info.ScreenPosition - new Vector2(size.X * info.Pivot.X, size.Y * info.Pivot.Y);
var labelRect = new RectF(topLeft.X, topLeft.Y, topLeft.X + size.X, topLeft.Y + size.Y); var labelRect = new RectF(topLeft.X, topLeft.Y, topLeft.X + size.X, topLeft.Y + size.Y);
// occlusion check
if (IsOccludedByAnyUi(labelRect)) if (IsOccludedByAnyUi(labelRect))
continue; continue;
drawList.AddScreenText(drawPos, info.Text, info.TextColor, info.FontSize, info.Pivot, info.EdgeColor, font); drawList.AddScreenText(drawPos, info.Text, info.TextColor, info.FontSize, info.Pivot, info.EdgeColor, font);
} }
} }
@@ -580,10 +639,10 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
private static uint PackColor(Vector4 color) private static uint PackColor(Vector4 color)
{ {
var r = (byte)System.Math.Clamp(color.X * 255f, 0f, 255f); var r = (byte)Math.Clamp(color.X * 255f, 0f, 255f);
var g = (byte)System.Math.Clamp(color.Y * 255f, 0f, 255f); var g = (byte)Math.Clamp(color.Y * 255f, 0f, 255f);
var b = (byte)System.Math.Clamp(color.Z * 255f, 0f, 255f); var b = (byte)Math.Clamp(color.Z * 255f, 0f, 255f);
var a = (byte)System.Math.Clamp(color.W * 255f, 0f, 255f); var a = (byte)Math.Clamp(color.W * 255f, 0f, 255f);
return (uint)((a << 24) | (b << 16) | (g << 8) | r); return (uint)((a << 24) | (b << 16) | (g << 8) | r);
} }
@@ -629,10 +688,19 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
if (scale <= 0f) if (scale <= 0f)
scale = 1f; scale = 1f;
var computed = (int)System.Math.Round(rawWidth * scale); var computed = (int)Math.Round(rawWidth * scale);
return System.Math.Max(1, computed); return Math.Max(1, computed);
} }
/// <summary>
/// Resolves a cached value for the given index.
/// </summary>
/// <param name="cache"></param>
/// <param name="index"></param>
/// <param name="rawValue"></param>
/// <param name="fallback"></param>
/// <param name="fallbackWhenZero"></param>
/// <returns></returns>
private static int ResolveCache( private static int ResolveCache(
int[] cache, int[] cache,
int index, int index,
@@ -660,7 +728,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
private bool TryCacheTextOffset(int nameplateIndex, int measuredTextWidth, int textOffset) private bool TryCacheTextOffset(int nameplateIndex, int measuredTextWidth, int textOffset)
{ {
if (System.Math.Abs(measuredTextWidth) > 0 || textOffset != 0) if (Math.Abs(measuredTextWidth) > 0 || textOffset != 0)
{ {
_buffers.TextOffsets[nameplateIndex] = textOffset; _buffers.TextOffsets[nameplateIndex] = textOffset;
return true; return true;
@@ -669,6 +737,12 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
return false; return false;
} }
/// <summary>
/// Snapping a position to pixel grid based on DPI scale.
/// </summary>
/// <param name="p">Position</param>
/// <param name="dpiScale">DPI Scale</param>
/// <returns></returns>
private static Vector2 SnapToPixels(Vector2 p, float dpiScale) private static Vector2 SnapToPixels(Vector2 p, float dpiScale)
{ {
// snap to pixel grid // snap to pixel grid
@@ -677,6 +751,15 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
return new Vector2(x, y); return new Vector2(x, y);
} }
/// <summary>
/// Smooths the position using exponential smoothing.
/// </summary>
/// <param name="idx">Nameplate Index</param>
/// <param name="target">Final position</param>
/// <param name="dt">Delta Time</param>
/// <param name="responsiveness">How responssive the smooting should be</param>
/// <returns></returns>
private Vector2 SmoothPosition(int idx, Vector2 target, float dt, float responsiveness = 24f) private Vector2 SmoothPosition(int idx, Vector2 target, float dt, float responsiveness = 24f)
{ {
// exponential smoothing // exponential smoothing
@@ -687,56 +770,78 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
return target; return target;
} }
// get current smoothed position
var cur = _buffers.SmoothedPos[idx]; var cur = _buffers.SmoothedPos[idx];
// compute smoothing factor // compute smoothing factor
var a = 1f - MathF.Exp(-responsiveness * dt); var a = 1f - MathF.Exp(-responsiveness * dt);
// snap if close enough
if (Vector2.DistanceSquared(cur, target) < 0.25f) if (Vector2.DistanceSquared(cur, target) < 0.25f)
return cur; return cur;
// lerp towards target
cur = Vector2.Lerp(cur, target, a); cur = Vector2.Lerp(cur, target, a);
_buffers.SmoothedPos[idx] = cur; _buffers.SmoothedPos[idx] = cur;
return cur; return cur;
} }
/// <summary>
/// Tries to get a valid screen rect for the given addon.
/// </summary>
/// <param name="addon">Addon UI</param>
/// <param name="screen">Screen positioning/param>
/// <param name="rect">RectF of Addon</param>
/// <returns></returns>
private static bool TryGetAddonRect(AtkUnitBase* addon, Vector2 screen, out RectF rect) private static bool TryGetAddonRect(AtkUnitBase* addon, Vector2 screen, out RectF rect)
{ {
// validate addon visibility and size // Addon existence
rect = default; rect = default;
if (addon == null) if (addon == null)
return false; return false;
// Visibility check
var root = addon->RootNode; var root = addon->RootNode;
if (root == null || !root->IsVisible()) if (root == null || !root->IsVisible())
return false; return false;
// Size check
float w = root->Width; float w = root->Width;
float h = root->Height; float h = root->Height;
if (w <= 0 || h <= 0) if (w <= 0 || h <= 0)
return false; return false;
// apply scale // Local scale
var sx = root->ScaleX; if (sx <= 0f) sx = 1f; float sx = root->ScaleX; if (sx <= 0f) sx = 1f;
var sy = root->ScaleY; if (sy <= 0f) sy = 1f; float sy = root->ScaleY; if (sy <= 0f) sy = 1f;
w *= sx; // World/composed scale from Transform
h *= sy; float wsx = GetWorldScaleX(root);
float wsy = GetWorldScaleY(root);
if (wsx <= 0f) wsx = 1f;
if (wsy <= 0f) wsy = 1f;
// World scale may include parent scaling; use it if meaningfully different.
float useX = MathF.Abs(wsx - sx) > 0.01f ? wsx : sx;
float useY = MathF.Abs(wsy - sy) > 0.01f ? wsy : sy;
w *= useX;
h *= useY;
if (w < 4f || h < 4f) if (w < 4f || h < 4f)
return false; return false;
// compute screen rect // Screen coords
float l = root->ScreenX; float l = root->ScreenX;
float t = root->ScreenY; float t = root->ScreenY;
float r = l + w; float r = l + w;
float b = t + h; float b = t + h;
// cull too large or out-of-bounds rects // Drop fullscreen-ish / insane rects
if (w >= screen.X * 0.98f && h >= screen.Y * 0.98f) if (w >= screen.X * 0.98f && h >= screen.Y * 0.98f)
return false; return false;
// Drop offscreen rects
if (l < -screen.X || t < -screen.Y || r > screen.X * 2f || b > screen.Y * 2f) if (l < -screen.X || t < -screen.Y || r > screen.X * 2f || b > screen.Y * 2f)
return false; return false;
@@ -744,6 +849,10 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
return true; return true;
} }
/// <summary>
/// Refreshes the cached UI rects for occlusion checking.
/// </summary>
/// <param name="unitMgr">Unit Manager</param>
private void RefreshUiRects(RaptureAtkUnitManager* unitMgr) private void RefreshUiRects(RaptureAtkUnitManager* unitMgr)
{ {
_uiRects.Clear(); _uiRects.Clear();
@@ -769,6 +878,11 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
} }
} }
/// <summary>
/// Is the given label rect occluded by any UI rects?
/// </summary>
/// <param name="labelRect">UI/Label Rect</param>
/// <returns>Is occluded or not</returns>
private bool IsOccludedByAnyUi(RectF labelRect) private bool IsOccludedByAnyUi(RectF labelRect)
{ {
for (int i = 0; i < _uiRects.Count; i++) for (int i = 0; i < _uiRects.Count; i++)
@@ -779,18 +893,33 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
return false; return false;
} }
/// <summary>
/// Gets the world scale X of the given node.
/// </summary>
/// <param name="n">Node</param>
/// <returns>World Scale of node</returns>
private static float GetWorldScaleX(AtkResNode* n) private static float GetWorldScaleX(AtkResNode* n)
{ {
var t = n->Transform; var t = n->Transform;
return MathF.Sqrt(t.M11 * t.M11 + t.M12 * t.M12); return MathF.Sqrt(t.M11 * t.M11 + t.M12 * t.M12);
} }
/// <summary>
/// Gets the world scale Y of the given node.
/// </summary>
/// <param name="n">Node</param>
/// <returns>World Scale of node</returns>
private static float GetWorldScaleY(AtkResNode* n) private static float GetWorldScaleY(AtkResNode* n)
{ {
var t = n->Transform; var t = n->Transform;
return MathF.Sqrt(t.M21 * t.M21 + t.M22 * t.M22); return MathF.Sqrt(t.M21 * t.M21 + t.M22 * t.M22);
} }
/// <summary>
/// Normalize an icon glyph input into a valid string.
/// </summary>
/// <param name="rawInput">Raw glyph input</param>
/// <returns>Normalized glyph input</returns>
internal static string NormalizeIconGlyph(string? rawInput) internal static string NormalizeIconGlyph(string? rawInput)
{ {
if (string.IsNullOrWhiteSpace(rawInput)) if (string.IsNullOrWhiteSpace(rawInput))
@@ -814,6 +943,11 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
return _defaultIconGlyph; return _defaultIconGlyph;
} }
/// <summary>
/// Is the nameplate addon visible?
/// </summary>
/// <returns>Is it visible?</returns>
private bool IsNamePlateAddonVisible() private bool IsNamePlateAddonVisible()
{ {
if (_mpNameplateAddon == null) if (_mpNameplateAddon == null)
@@ -823,6 +957,11 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
return root != null && root->IsVisible(); return root != null && root->IsVisible();
} }
/// <summary>
/// Converts raw icon glyph input into an icon editor string.
/// </summary>
/// <param name="rawInput">Raw icon glyph input</param>
/// <returns>Icon editor string</returns>
internal static string ToIconEditorString(string? rawInput) internal static string ToIconEditorString(string? rawInput)
{ {
var normalized = NormalizeIconGlyph(rawInput); var normalized = NormalizeIconGlyph(rawInput);
@@ -831,6 +970,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
? runeEnumerator.Current.Value.ToString("X4", CultureInfo.InvariantCulture) ? runeEnumerator.Current.Value.ToString("X4", CultureInfo.InvariantCulture)
: _defaultIconGlyph; : _defaultIconGlyph;
} }
private readonly struct NameplateLabelInfo private readonly struct NameplateLabelInfo
{ {
public NameplateLabelInfo( public NameplateLabelInfo(
@@ -860,6 +1000,9 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
public bool UseIcon { get; } public bool UseIcon { get; }
} }
/// <summary>
/// Visible paired user IDs snapshot.
/// </summary>
private HashSet<ulong> VisibleUserIds private HashSet<ulong> VisibleUserIds
=> [.. _pairUiService.GetSnapshot().PairsByUid.Values => [.. _pairUiService.GetSnapshot().PairsByUid.Values
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue) .Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
@@ -879,6 +1022,10 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
} }
} }
/// <summary>
/// Update the active broadcasting CIDs.
/// </summary>
/// <param name="cids">Inbound new CIDs</param>
public void UpdateBroadcastingCids(IEnumerable<string> cids) public void UpdateBroadcastingCids(IEnumerable<string> cids)
{ {
var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal); var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal);
@@ -891,6 +1038,9 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
FlagRefresh(); FlagRefresh();
} }
/// <summary>
/// Clears all nameplate related caches.
/// </summary>
public void ClearNameplateCaches() public void ClearNameplateCaches()
{ {
_buffers.Clear(); _buffers.Clear();
@@ -929,18 +1079,31 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
} }
} }
/// <summary>
/// Starts the LightFinder Plate Handler.
/// </summary>
/// <param name="cancellationToken">Cancellation Token</param>
/// <returns>Task Completed</returns>
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
{ {
Init(); Init();
return Task.CompletedTask; return Task.CompletedTask;
} }
/// <summary>
/// Stops the LightFinder Plate Handler.
/// </summary>
/// <param name="cancellationToken">Cancellation Token</param>
/// <returns>Task Completed</returns>
public Task StopAsync(CancellationToken cancellationToken) public Task StopAsync(CancellationToken cancellationToken)
{ {
Uninit(); Uninit();
return Task.CompletedTask; return Task.CompletedTask;
} }
/// <summary>
/// Rectangle with float coordinates for intersection testing.
/// </summary>
[StructLayout(LayoutKind.Auto)] [StructLayout(LayoutKind.Auto)]
private readonly struct RectF private readonly struct RectF
{ {