2.0.2 merged again

This commit is contained in:
cake
2025-12-28 16:40:47 +01:00
30 changed files with 1086 additions and 388 deletions

View File

@@ -23,6 +23,7 @@ using Pictomancy;
using System.Collections.Immutable;
using System.Globalization;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Task = System.Threading.Tasks.Task;
@@ -41,6 +42,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
private readonly LightlessConfigService _configService;
private readonly PairUiService _pairUiService;
private readonly LightlessMediator _mediator;
public LightlessMediator Mediator => _mediator;
private readonly IUiBuilder _uiBuilder;
@@ -61,16 +63,22 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
// / Overlay window flags
private const ImGuiWindowFlags _overlayFlags =
ImGuiWindowFlags.NoDecoration |
ImGuiWindowFlags.NoBackground |
ImGuiWindowFlags.NoMove |
ImGuiWindowFlags.NoSavedSettings |
ImGuiWindowFlags.NoNav |
ImGuiWindowFlags.NoInputs;
ImGuiWindowFlags.NoDecoration |
ImGuiWindowFlags.NoBackground |
ImGuiWindowFlags.NoMove |
ImGuiWindowFlags.NoSavedSettings |
ImGuiWindowFlags.NoNav |
ImGuiWindowFlags.NoInputs;
private readonly List<RectF> _uiRects = new(128);
private ImmutableHashSet<string> _activeBroadcastingCids = [];
#if DEBUG
// Debug controls
// Debug counters (read-only from UI)
#endif
private bool IsPictomancyRenderer => _configService.Current.LightfinderLabelRenderer == LightfinderLabelRenderer.Pictomancy;
public LightFinderPlateHandler(
@@ -96,7 +104,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
_uiBuilder = pluginInterface.UiBuilder ?? throw new ArgumentNullException(nameof(pluginInterface));
_ = pictomancyService ?? throw new ArgumentNullException(nameof(pictomancyService));
_lastRenderer = _configService.Current.LightfinderLabelRenderer;
}
private void RefreshRendererState()
@@ -187,8 +194,6 @@ 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)
{
RefreshRendererState();
@@ -199,6 +204,16 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
return;
}
// Hide our overlay when the user hides the entire game UI (ScrollLock).
if (_gameGui.GameUiHidden)
{
ClearLabelBuffer();
Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
_lastNamePlateDrawFrame = 0;
return;
}
// gpose: do not draw.
if (_clientState.IsGPosing)
{
ClearLabelBuffer();
@@ -218,6 +233,10 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
if (fw != null)
_lastNamePlateDrawFrame = fw->FrameCounter;
#if DEBUG
DebugLastNameplateFrame = _lastNamePlateDrawFrame;
#endif
var pNameplateAddon = (AddonNamePlate*)args.Addon.Address;
if (_mpNameplateAddon != pNameplateAddon)
@@ -234,6 +253,13 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
/// </summary>
private void UpdateNameplateNodes()
{
// If the user has hidden the UI, don't compute any labels.
if (_gameGui.GameUiHidden)
{
ClearLabelBuffer();
return;
}
var currentHandle = _gameGui.GetAddonByName("NamePlate");
if (currentHandle.Address == nint.Zero)
{
@@ -297,7 +323,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
for (int i = 0; i < safeCount; ++i)
{
var objectInfoPtr = vec[i];
if (objectInfoPtr == null)
continue;
@@ -314,7 +339,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
if ((ObjectKind)gameObject->ObjectKind != ObjectKind.Player)
continue;
// CID gating
// CID gating - only show for active broadcasters
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)gameObject);
if (cid == null || !_activeBroadcastingCids.Contains(cid))
continue;
@@ -350,12 +375,12 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
if (!currentConfig.LightfinderLabelShowHidden && !isNameplateVisible)
continue;
// Prepare label content and scaling
var scaleMultiplier = System.Math.Clamp(currentConfig.LightfinderLabelScale, 0.5f, 2.0f);
// Prepare label content and scaling factors
var scaleMultiplier = Math.Clamp(currentConfig.LightfinderLabelScale, 0.5f, 2.0f);
var baseScale = currentConfig.LightfinderLabelUseIcon ? 1.0f : 0.5f;
var effectiveScale = baseScale * scaleMultiplier;
var baseFontSize = currentConfig.LightfinderLabelUseIcon ? 36f : 24f;
var targetFontSize = (int)System.Math.Round(baseFontSize * scaleMultiplier);
var targetFontSize = (int)Math.Round(baseFontSize * scaleMultiplier);
var labelContent = currentConfig.LightfinderLabelUseIcon
? NormalizeIconGlyph(currentConfig.LightfinderLabelIconGlyph)
: _defaultLabelText;
@@ -363,8 +388,8 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
if (!currentConfig.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal)))
labelContent = _defaultLabelText;
var nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
var nodeHeight = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale);
var nodeWidth = (int)Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
var nodeHeight = (int)Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale);
AlignmentType alignment;
var textScaleY = nameText->AtkResNode.ScaleY;
@@ -374,7 +399,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
var blockHeight = ResolveCache(
_buffers.TextHeights,
nameplateIndex,
System.Math.Abs((int)nameplateObject.TextH),
Math.Abs((int)nameplateObject.TextH),
() => GetScaledTextHeight(nameText),
nodeHeight);
@@ -384,7 +409,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
(int)nameContainer->Height,
() =>
{
var computed = blockHeight + (int)System.Math.Round(8 * textScaleY);
var computed = blockHeight + (int)Math.Round(8 * textScaleY);
return computed <= blockHeight ? blockHeight + 1 : computed;
},
blockHeight + 1);
@@ -392,7 +417,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
var blockTop = containerHeight - blockHeight;
if (blockTop < 0)
blockTop = 0;
var verticalPadding = (int)System.Math.Round(4 * effectiveScale);
var verticalPadding = (int)Math.Round(4 * effectiveScale);
var positionY = blockTop - verticalPadding;
@@ -400,21 +425,14 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
var textWidth = ResolveCache(
_buffers.TextWidths,
nameplateIndex,
System.Math.Abs(rawTextWidth),
Math.Abs(rawTextWidth),
() => GetScaledTextWidth(nameText),
nodeWidth);
// Text offset caching
var textOffset = (int)System.Math.Round(nameText->AtkResNode.X);
var textOffset = (int)Math.Round(nameText->AtkResNode.X);
var hasValidOffset = TryCacheTextOffset(nameplateIndex, rawTextWidth, textOffset);
if (nameContainer == null)
{
if (_logger.IsEnabled(LogLevel.Debug))
_logger.LogDebug("Nameplate {Index} container became unavailable during update, skipping.", nameplateIndex);
continue;
}
var res = nameContainer;
// X scale
@@ -450,7 +468,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
var offsetXScreen = currentConfig.LightfinderLabelOffsetX * worldScaleX;
// alignment based on config
// alignment based on config setting
switch (currentConfig.LabelAlignment)
{
case LabelAlignment.Left:
@@ -469,7 +487,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
}
else
{
// manual X positioning
// manual X positioning with optional cached offset
var cachedTextOffset = _buffers.TextOffsets[nameplateIndex];
var hasCachedOffset = cachedTextOffset != int.MinValue;
var baseOffsetXLocal = (!currentConfig.LightfinderLabelUseIcon && hasValidOffset && hasCachedOffset)
@@ -489,16 +507,16 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
// final position before smoothing
var finalPosition = new Vector2(finalX, res->ScreenY + positionYScreen);
var dpiScale = ImGui.GetIO().DisplayFramebufferScale.X; // often same for Y
var dpiScale = ImGui.GetIO().DisplayFramebufferScale.X;
var fw = Framework.Instance();
float dt = fw->RealFrameDeltaTime;
//smoothing..
//smoothing.. snap.. smooth.. snap
finalPosition = SnapToPixels(finalPosition, dpiScale);
finalPosition = SmoothPosition(nameplateIndex, finalPosition, dt);
finalPosition = SnapToPixels(finalPosition, dpiScale);
// prepare label info
// prepare label info for rendering
var pivot = (currentConfig.LightfinderAutoAlign || currentConfig.LightfinderLabelUseIcon)
? AlignmentToPivot(alignment)
: _defaultPivot;
@@ -545,7 +563,23 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
if (fw == null)
return;
// Frame skip check
// If UI is hidden, do not render.
if (_gameGui.GameUiHidden)
{
ClearLabelBuffer();
Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
_lastNamePlateDrawFrame = 0;
#if DEBUG
DebugLabelCountLastFrame = 0;
DebugUiRectCountLastFrame = 0;
DebugOccludedCountLastFrame = 0;
DebugLastNameplateFrame = 0;
#endif
return;
}
// Frame skip check - skip if more than 1 frame has passed since last nameplate draw.
var frame = fw->FrameCounter;
if (_lastNamePlateDrawFrame == 0 || (frame - _lastNamePlateDrawFrame) > 1)
@@ -553,34 +587,62 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
ClearLabelBuffer();
Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
#if DEBUG
DebugLabelCountLastFrame = 0;
DebugUiRectCountLastFrame = 0;
DebugOccludedCountLastFrame = 0;
DebugLastNameplateFrame = _lastNamePlateDrawFrame;
#endif
return;
}
//Gpose Check
// Gpose Check - do not render.
if (_clientState.IsGPosing)
{
ClearLabelBuffer();
Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
_lastNamePlateDrawFrame = 0;
#if DEBUG
DebugLabelCountLastFrame = 0;
DebugUiRectCountLastFrame = 0;
DebugOccludedCountLastFrame = 0;
DebugLastNameplateFrame = 0;
#endif
return;
}
// If nameplate addon is not visible, skip rendering
// If nameplate addon is not visible, skip rendering entirely.
if (!IsNamePlateAddonVisible())
{
#if DEBUG
DebugLabelCountLastFrame = 0;
DebugUiRectCountLastFrame = 0;
DebugOccludedCountLastFrame = 0;
DebugLastNameplateFrame = _lastNamePlateDrawFrame;
#endif
return;
}
int copyCount;
lock (_labelLock)
{
copyCount = _labelRenderCount;
if (copyCount == 0)
{
#if DEBUG
DebugLabelCountLastFrame = 0;
DebugUiRectCountLastFrame = 0;
DebugOccludedCountLastFrame = 0;
DebugLastNameplateFrame = _lastNamePlateDrawFrame;
#endif
return;
}
Array.Copy(_buffers.LabelRender, _buffers.LabelCopy, copyCount);
}
var uiModule = fw != null ? fw->GetUIModule() : null;
var uiModule = fw->GetUIModule();
if (uiModule != null)
{
var rapture = uiModule->GetRaptureAtkModule();
@@ -599,7 +661,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
var vpPos = vp.Pos;
ImGuiHelpers.ForceNextWindowMainViewport();
ImGui.SetNextWindowPos(vp.Pos);
ImGui.SetNextWindowSize(vp.Size);
@@ -610,54 +671,118 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
ImGui.PopStyleVar(2);
using var drawList = PictoService.Draw();
if (drawList == null)
// Debug flags
bool dbgEnabled = false;
bool dbgDisableOcc = false;
bool dbgDrawUiRects = false;
bool dbgDrawLabelRects = false;
#if DEBUG
dbgEnabled = DebugEnabled;
dbgDisableOcc = DebugDisableOcclusion;
dbgDrawUiRects = DebugDrawUiRects;
dbgDrawLabelRects = DebugDrawLabelRects;
#endif
int occludedThisFrame = 0;
try
{
using var drawList = PictoService.Draw();
if (drawList == null)
return;
// Debug drawing uses the window drawlist (so it always draws in the correct viewport).
var dbgDl = ImGui.GetWindowDrawList();
var useViewportOffset = ImGui.GetIO().ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable);
for (int i = 0; i < copyCount; ++i)
{
ref var info = ref _buffers.LabelCopy[i];
// final draw position with viewport offset (only when viewports are enabled)
var drawPos = info.ScreenPosition;
if (useViewportOffset)
drawPos += vpPos;
var font = default(ImFontPtr);
if (info.UseIcon)
{
var ioFonts = ImGui.GetIO().Fonts;
font = ioFonts.Fonts.Size > 1 ? new ImFontPtr(ioFonts.Fonts[1]) : ImGui.GetFont();
}
else
{
font = ImGui.GetFont();
}
if (!font.IsNull)
ImGui.PushFont(font);
// calculate size for occlusion checking
var baseSize = ImGui.CalcTextSize(info.Text);
var baseFontSize = ImGui.GetFontSize();
if (!font.IsNull)
ImGui.PopFont();
// scale size based on font size
var scale = baseFontSize > 0 ? (info.FontSize / baseFontSize) : 1f;
var size = baseSize * scale;
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);
bool wouldOcclude = IsOccludedByAnyUi(labelRect);
if (wouldOcclude)
occludedThisFrame++;
// Debug: draw label rects
if (dbgEnabled && dbgDrawLabelRects)
{
var tl = new Vector2(labelRect.L, labelRect.T);
var br = new Vector2(labelRect.R, labelRect.B);
if (useViewportOffset) { tl += vpPos; br += vpPos; }
// green = visible, red = would be occluded (even if forced)
var col = wouldOcclude
? ImGui.GetColorU32(new Vector4(1f, 0f, 0f, 0.6f))
: ImGui.GetColorU32(new Vector4(0f, 1f, 0f, 0.6f));
dbgDl.AddRect(tl, br, col);
}
// occlusion check (allow debug to disable)
if (!dbgDisableOcc && wouldOcclude)
continue;
drawList.AddScreenText(drawPos, info.Text, info.TextColor, info.FontSize, info.Pivot, info.EdgeColor, font);
}
// Debug: draw UI rects if any
if (dbgEnabled && dbgDrawUiRects && _uiRects.Count > 0)
{
var useOff = useViewportOffset ? vpPos : Vector2.Zero;
var col = ImGui.GetColorU32(new Vector4(1f, 1f, 1f, 0.35f));
for (int i = 0; i < _uiRects.Count; i++)
{
var r = _uiRects[i];
dbgDl.AddRect(new Vector2(r.L, r.T) + useOff, new Vector2(r.R, r.B) + useOff, col);
}
}
}
finally
{
ImGui.End();
return;
}
for (int i = 0; i < copyCount; ++i)
{
ref var info = ref _buffers.LabelCopy[i];
// final draw position with viewport offset
var drawPos = info.ScreenPosition + vpPos;
var font = default(ImFontPtr);
if (info.UseIcon)
{
var ioFonts = ImGui.GetIO().Fonts;
font = ioFonts.Fonts.Size > 1 ? new ImFontPtr(ioFonts.Fonts[1]) : ImGui.GetFont();
}
else
{
font = ImGui.GetFont();
}
if (!font.IsNull)
ImGui.PushFont(font);
// calculate size for occlusion checking
var baseSize = ImGui.CalcTextSize(info.Text);
var baseFontSize = ImGui.GetFontSize();
if (!font.IsNull)
ImGui.PopFont();
// scale size based on font size
var scale = baseFontSize > 0 ? (info.FontSize / baseFontSize) : 1f;
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 labelRect = new RectF(topLeft.X, topLeft.Y, topLeft.X + size.X, topLeft.Y + size.Y);
// occlusion check
if (IsOccludedByAnyUi(labelRect))
continue;
drawList.AddScreenText(drawPos, info.Text, info.TextColor, info.FontSize, info.Pivot, info.EdgeColor, font);
}
#if DEBUG
DebugLabelCountLastFrame = copyCount;
DebugUiRectCountLastFrame = _uiRects.Count;
DebugOccludedCountLastFrame = occludedThisFrame;
DebugLastNameplateFrame = _lastNamePlateDrawFrame;
#endif
}
private static Vector2 AlignmentToPivot(AlignmentType alignment) => alignment switch
@@ -705,8 +830,8 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
if (scale <= 0f)
scale = 1f;
var computed = (int)System.Math.Round(rawHeight * scale);
return System.Math.Max(1, computed);
var computed = (int)Math.Round(rawHeight * scale);
return Math.Max(1, computed);
}
private static unsafe int GetScaledTextWidth(AtkTextNode* node)
@@ -730,12 +855,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
/// <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(
int[] cache,
int index,
@@ -775,9 +894,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
/// <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)
{
// snap to pixel grid
@@ -786,15 +902,9 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
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)
{
// exponential smoothing
@@ -812,7 +922,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
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;
// lerp towards target
@@ -821,73 +931,193 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
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>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsFinite(float f) => !(float.IsNaN(f) || float.IsInfinity(f));
private static bool TryGetAddonRect(AtkUnitBase* addon, Vector2 screen, out RectF rect)
{
// Addon existence
rect = default;
if (addon == null)
return false;
// Visibility check
// Addon must be visible
if (!addon->IsVisible)
return false;
// Root must be visible
var root = addon->RootNode;
if (root == null || !root->IsVisible())
return false;
// Size check
float w = root->Width;
float h = root->Height;
if (w <= 0 || h <= 0)
// Must have multiple nodes to be useful
var nodeCount = addon->UldManager.NodeListCount;
var nodeList = addon->UldManager.NodeList;
if (nodeCount <= 1 || nodeList == null)
return false;
// Local scale
float sx = root->ScaleX; if (sx <= 0f) sx = 1f;
float sy = root->ScaleY; if (sy <= 0f) sy = 1f;
float rsx = GetWorldScaleX(root);
float rsy = GetWorldScaleY(root);
if (!IsFinite(rsx) || rsx <= 0f) rsx = 1f;
if (!IsFinite(rsy) || rsy <= 0f) rsy = 1f;
// World/composed scale from Transform
float wsx = GetWorldScaleX(root);
float wsy = GetWorldScaleY(root);
if (wsx <= 0f) wsx = 1f;
if (wsy <= 0f) wsy = 1f;
// clamp insane root scales (rare but prevents explosions)
rsx = MathF.Min(rsx, 6f);
rsy = MathF.Min(rsy, 6f);
// 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)
float rw = root->Width * rsx;
float rh = root->Height * rsy;
if (!IsFinite(rw) || !IsFinite(rh) || rw <= 2f || rh <= 2f)
return false;
// Screen coords
float l = root->ScreenX;
float t = root->ScreenY;
float r = l + w;
float b = t + h;
// Drop fullscreen-ish / insane rects
if (w >= screen.X * 0.98f && h >= screen.Y * 0.98f)
float rl = root->ScreenX;
float rt = root->ScreenY;
if (!IsFinite(rl) || !IsFinite(rt))
return false;
// Drop offscreen rects
if (l < -screen.X || t < -screen.Y || r > screen.X * 2f || b > screen.Y * 2f)
float rr = rl + rw;
float rb = rt + rh;
// If root is basically fullscreen, it<69>s not a useful occluder for our purpose.
if (rw >= screen.X * 0.98f && rh >= screen.Y * 0.98f)
return false;
// Clip root to screen so it stays sane
float rootL = MathF.Max(0f, rl);
float rootT = MathF.Max(0f, rt);
float rootR = MathF.Min(screen.X, rr);
float rootB = MathF.Min(screen.Y, rb);
if (rootR <= rootL || rootB <= rootT)
return false;
// Root dimensions
var rootW = rootR - rootL;
var rootH = rootB - rootT;
// Find union of all probably-drawable nodes intersecting root
bool any = false;
float l = float.MaxValue, t = float.MaxValue, r = float.MinValue, b = float.MinValue;
// Allow a small bleed outside root; some addons draw small bits outside their root container.
const float rootPad = 24f;
float padL = rootL - rootPad;
float padT = rootT - rootPad;
float padR = rootR + rootPad;
float padB = rootB + rootPad;
for (int i = 1; i < nodeCount; i++)
{
var n = nodeList[i];
if (!IsProbablyDrawableNode(n))
continue;
float w = n->Width;
float h = n->Height;
if (!IsFinite(w) || !IsFinite(h) || w <= 1f || h <= 1f)
continue;
float sx = GetWorldScaleX(n);
float sy = GetWorldScaleY(n);
if (!IsFinite(sx) || sx <= 0f) sx = 1f;
if (!IsFinite(sy) || sy <= 0f) sy = 1f;
sx = MathF.Min(sx, 6f);
sy = MathF.Min(sy, 6f);
w *= sx;
h *= sy;
if (!IsFinite(w) || !IsFinite(h) || w < 2f || h < 2f)
continue;
float nl = n->ScreenX;
float nt = n->ScreenY;
if (!IsFinite(nl) || !IsFinite(nt))
continue;
float nr = nl + w;
float nb = nt + h;
// Must intersect root (with padding). This is the big mitigation.
if (nr <= padL || nb <= padT || nl >= padR || nt >= padB)
continue;
// Reject nodes that are wildly larger than the root (common on targeting).
if (w > rootW * 2.0f || h > rootH * 2.0f)
continue;
// Clip node to root and then to screen (prevents offscreen junk stretching union)
float cl = MathF.Max(rootL, nl);
float ct = MathF.Max(rootT, nt);
float cr = MathF.Min(rootR, nr);
float cb = MathF.Min(rootB, nb);
cl = MathF.Max(0f, cl);
ct = MathF.Max(0f, ct);
cr = MathF.Min(screen.X, cr);
cb = MathF.Min(screen.Y, cb);
if (cr <= cl || cb <= ct)
continue;
any = true;
if (cl < l) l = cl;
if (ct < t) t = ct;
if (cr > r) r = cr;
if (cb > b) b = cb;
}
// If nothing usable, fallback to root rect (still a sane occluder)
if (!any)
{
rect = new RectF(rootL, rootT, rootR, rootB);
return true;
}
// Validate final union rect
var uw = r - l;
var uh = b - t;
if (uw < 4f || uh < 4f)
{
rect = new RectF(rootL, rootT, rootR, rootB);
return true;
}
// If union is excessively larger than root, fallback to root rect
if (uw > rootW * 1.35f || uh > rootH * 1.35f)
{
rect = new RectF(rootL, rootT, rootR, rootB);
return true;
}
rect = new RectF(l, t, r, b);
return true;
}
private static bool IsProbablyDrawableNode(AtkResNode* n)
{
if (n == null || !n->IsVisible())
return false;
// Check alpha
if (n->Color.A == 16)
return false;
// Check node type
return n->Type switch
{
NodeType.Text => true,
NodeType.Image => true,
NodeType.NineGrid => true,
NodeType.Counter => true,
NodeType.Component => true,
_ => false,
};
}
/// <summary>
/// Refreshes the cached UI rects for occlusion checking.
/// </summary>
/// <param name="unitMgr">Unit Manager</param>
private void RefreshUiRects(RaptureAtkUnitManager* unitMgr)
{
_uiRects.Clear();
@@ -911,13 +1141,15 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
if (TryGetAddonRect(addon, screen, out var r))
_uiRects.Add(r);
}
#if DEBUG
DebugUiRectCountLastFrame = _uiRects.Count;
#endif
}
/// <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)
{
for (int i = 0; i < _uiRects.Count; i++)
@@ -931,8 +1163,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
/// <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)
{
var t = n->Transform;
@@ -942,8 +1172,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
/// <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)
{
var t = n->Transform;
@@ -953,8 +1181,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
/// <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)
{
if (string.IsNullOrWhiteSpace(rawInput))
@@ -982,7 +1208,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
/// <summary>
/// Is the nameplate addon visible?
/// </summary>
/// <returns>Is it visible?</returns>
private bool IsNamePlateAddonVisible()
{
if (_mpNameplateAddon == null)
@@ -992,20 +1217,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
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)
{
var normalized = NormalizeIconGlyph(rawInput);
var runeEnumerator = normalized.EnumerateRunes();
return runeEnumerator.MoveNext()
? runeEnumerator.Current.Value.ToString("X4", CultureInfo.InvariantCulture)
: _defaultIconGlyph;
}
private readonly struct NameplateLabelInfo
{
public NameplateLabelInfo(
@@ -1043,6 +1254,15 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
.Select(u => (ulong)u.PlayerCharacterId)];
public int DebugLabelCountLastFrame { get; set; }
public int DebugUiRectCountLastFrame { get; set; }
public int DebugOccludedCountLastFrame { get; set; }
public uint DebugLastNameplateFrame { get; set; }
public bool DebugDrawUiRects { get; set; }
public bool DebugDrawLabelRects { get; set; } = true;
public bool DebugDisableOcclusion { get; set; }
public bool DebugEnabled { get; set; }
public void FlagRefresh()
{
_needsLabelRefresh = true;
@@ -1066,7 +1286,6 @@ 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)
{
var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal);
@@ -1096,7 +1315,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
public NameplateBuffers()
{
TextOffsets = new int[AddonNamePlate.NumNamePlateObjects];
System.Array.Fill(TextOffsets, int.MinValue);
Array.Fill(TextOffsets, int.MinValue);
}
public int[] TextWidths { get; } = new int[AddonNamePlate.NumNamePlateObjects];
@@ -1108,23 +1327,20 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
public NameplateLabelInfo[] LabelCopy { get; } = new NameplateLabelInfo[AddonNamePlate.NumNamePlateObjects];
public Vector2[] SmoothedPos = new Vector2[AddonNamePlate.NumNamePlateObjects];
public bool[] HasSmoothed = new bool[AddonNamePlate.NumNamePlateObjects];
public void Clear()
{
System.Array.Clear(TextWidths, 0, TextWidths.Length);
System.Array.Clear(TextHeights, 0, TextHeights.Length);
System.Array.Clear(ContainerHeights, 0, ContainerHeights.Length);
System.Array.Fill(TextOffsets, int.MinValue);
Array.Clear(TextWidths, 0, TextWidths.Length);
Array.Clear(TextHeights, 0, TextHeights.Length);
Array.Clear(ContainerHeights, 0, ContainerHeights.Length);
Array.Fill(TextOffsets, int.MinValue);
}
}
/// <summary>
/// Starts the LightFinder Plate Handler.
/// </summary>
/// <param name="cancellationToken">Cancellation Token</param>
/// <returns>Task Completed</returns>
public Task StartAsync(CancellationToken cancellationToken)
{
Init();
@@ -1134,8 +1350,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
/// <summary>
/// Stops the LightFinder Plate Handler.
/// </summary>
/// <param name="cancellationToken">Cancellation Token</param>
/// <returns>Task Completed</returns>
public Task StopAsync(CancellationToken cancellationToken)
{
Uninit();
@@ -1154,4 +1368,4 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
public bool Intersects(in RectF o) =>
!(R <= o.L || o.R <= L || B <= o.T || o.B <= T);
}
}
}

View File

@@ -67,7 +67,7 @@ public class LightFinderService : IHostedService, IMediatorSubscriber
{
try
{
var cid = await _dalamudUtil.GetCIDAsync().ConfigureAwait(false);
var cid = _dalamudUtil.GetCID();
return cid.ToString().GetHash256();
}
catch (Exception ex)