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