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/DalamudUtilService.cs b/LightlessSync/Services/DalamudUtilService.cs index c8668eb..106adf2 100644 --- a/LightlessSync/Services/DalamudUtilService.cs +++ b/LightlessSync/Services/DalamudUtilService.cs @@ -327,8 +327,8 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber public IEnumerable GetGposeCharactersFromObjectTable() { - foreach (var actor in _actorObjectService.PlayerDescriptors - .Where(a => a.ObjectKind == DalamudObjectKind.Player && a.ObjectIndex > 200)) + foreach (var actor in _objectTable + .Where(a => a.ObjectIndex > 200 && a.ObjectKind == DalamudObjectKind.Player)) { var character = _objectTable.CreateObjectReference(actor.Address) as ICharacter; if (character != null) @@ -490,6 +490,10 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber { EnsureIsOnFramework(); var playerChar = GetPlayerCharacter(); + + if (playerChar == null || playerChar.Address == IntPtr.Zero) + return 0; + return ((BattleChara*)playerChar.Address)->Character.ContentId; } diff --git a/LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs b/LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs index d78563c..5048ab7 100644 --- a/LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs +++ b/LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs @@ -4,6 +4,7 @@ using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.Text; using Dalamud.Interface; +using Dalamud.Interface.Utility; using Dalamud.Plugin; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.System.Framework; @@ -16,22 +17,27 @@ using LightlessSync.UI; using LightlessSync.UI.Services; using LightlessSync.Utils; using LightlessSync.UtilsEnum.Enum; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using Pictomancy; using System.Collections.Immutable; using System.Globalization; using System.Numerics; +using System.Runtime.InteropServices; 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; @@ -42,21 +48,33 @@ 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 static readonly Vector2 DefaultPivot = new(0.5f, 1f); + 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 | + ImGuiWindowFlags.NoMove | + ImGuiWindowFlags.NoSavedSettings | + ImGuiWindowFlags.NoNav | + ImGuiWindowFlags.NoInputs; + + private readonly List _uiRects = new(128); private ImmutableHashSet _activeBroadcastingCids = []; public LightFinderPlateHandler( ILogger logger, IAddonLifecycle addonLifecycle, IGameGui gameGui, + IClientState clientState, LightlessConfigService configService, LightlessMediator mediator, IObjectTable objectTable, @@ -67,6 +85,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe _logger = logger; _addonLifecycle = addonLifecycle; _gameGui = gameGui; + _clientState = clientState; _configService = configService; _mediator = mediator; _objectTable = objectTable; @@ -101,6 +120,9 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe _mpNameplateAddon = null; } + /// + /// Enable nameplate handling. + /// internal void EnableNameplate() { if (!_mEnabled) @@ -118,6 +140,9 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe } } + /// + /// Disable nameplate handling. + /// internal void DisableNameplate() { if (_mEnabled) @@ -136,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)) @@ -145,6 +183,10 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe return; } + var fw = Framework.Instance(); + if (fw != null) + _lastNamePlateDrawFrame = fw->FrameCounter; + var pNameplateAddon = (AddonNamePlate*)args.Addon.Address; if (_mpNameplateAddon != pNameplateAddon) @@ -156,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"); @@ -175,6 +220,12 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe return; } + if (!IsNamePlateAddonVisible()) + { + ClearLabelBuffer(); + return; + } + var framework = Framework.Instance(); if (framework == null) { @@ -207,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"); @@ -215,6 +266,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe for (int i = 0; i < safeCount; ++i) { + var objectInfoPtr = vec[i]; if (objectInfoPtr == null) continue; @@ -250,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) { @@ -261,14 +312,14 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe root->Component->UldManager.UpdateDrawNodeList(); - bool isVisible = - (marker != null && marker->AtkResNode.IsVisible()) || - (nameContainer->IsVisible() && nameText->AtkResNode.IsVisible()) || - currentConfig.LightfinderLabelShowHidden; + bool isNameplateVisible = + nameContainer->IsVisible() && + nameText->AtkResNode.IsVisible(); - if (!isVisible) + 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; @@ -276,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); @@ -322,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); @@ -332,65 +384,93 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe continue; } + 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; + + positionY += currentConfig.LightfinderLabelOffsetY; + var positionYScreen = positionY * worldScaleY; + float finalX; if (currentConfig.LightfinderAutoAlign) { - var measuredWidth = System.Math.Max(1, textWidth > 0 ? textWidth : nodeWidth); + // auto X positioning + var measuredWidth = Math.Max(1, textWidth > 0 ? textWidth : nodeWidth); var measuredWidthF = (float)measuredWidth; - var alignmentType = currentConfig.LabelAlignment; - var containerScale = nameContainer->ScaleX; - if (containerScale <= 0f) - containerScale = 1f; - var containerWidthRaw = (float)nameContainer->Width; - if (containerWidthRaw <= 0f) - containerWidthRaw = measuredWidthF; - var containerWidth = containerWidthRaw * containerScale; - if (containerWidth <= 0f) - containerWidth = measuredWidthF; + // consider icon width + var containerWidthLocal = res->Width > 0 ? res->Width : measuredWidthF; + var containerWidthScreen = containerWidthLocal * worldScaleX; - var containerLeft = nameContainer->ScreenX; - var containerRight = containerLeft + containerWidth; - var containerCenter = containerLeft + (containerWidth * 0.5f); + // container bounds for positions + var containerLeft = res->ScreenX; + var containerRight = containerLeft + containerWidthScreen; + var containerCenter = containerLeft + (containerWidthScreen * 0.5f); var iconMargin = currentConfig.LightfinderLabelUseIcon - ? System.Math.Min(containerWidth * 0.1f, 14f * containerScale) + ? MathF.Min(containerWidthScreen * 0.1f, 14f * worldScaleX) : 0f; - switch (alignmentType) + var offsetXScreen = currentConfig.LightfinderLabelOffsetX * worldScaleX; + + // alignment based on config + switch (currentConfig.LabelAlignment) { case LabelAlignment.Left: - finalX = containerLeft + iconMargin; + finalX = containerLeft + iconMargin + offsetXScreen; alignment = AlignmentType.BottomLeft; break; case LabelAlignment.Right: - finalX = containerRight - iconMargin; + finalX = containerRight - iconMargin + offsetXScreen; alignment = AlignmentType.BottomRight; break; default: - finalX = containerCenter; + finalX = containerCenter + offsetXScreen; alignment = AlignmentType.Bottom; break; } - - finalX += currentConfig.LightfinderLabelOffsetX; } else { + // manual X positioning var cachedTextOffset = _buffers.TextOffsets[nameplateIndex]; var hasCachedOffset = cachedTextOffset != int.MinValue; - var baseOffsetX = (!currentConfig.LightfinderLabelUseIcon && hasValidOffset && hasCachedOffset) ? cachedTextOffset : 0; - finalX = nameContainer->ScreenX + baseOffsetX + 58 + currentConfig.LightfinderLabelOffsetX; + var baseOffsetXLocal = (!currentConfig.LightfinderLabelUseIcon && hasValidOffset && hasCachedOffset) + ? cachedTextOffset + : 0; + + finalX = + res->ScreenX + + (baseOffsetXLocal * worldScaleX) + + (58f * worldScaleX) + + (currentConfig.LightfinderLabelOffsetX * worldScaleX); + alignment = AlignmentType.Bottom; } - positionY += currentConfig.LightfinderLabelOffsetY; - alignment = (AlignmentType)System.Math.Clamp((int)alignment, 0, 8); + alignment = (AlignmentType)Math.Clamp((int)alignment, 0, 8); - var finalPosition = new Vector2(finalX, nameContainer->ScreenY + positionY); + // 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(); + float dt = fw->RealFrameDeltaTime; + + //smoothing.. + finalPosition = SnapToPixels(finalPosition, dpiScale); + finalPosition = SmoothPosition(nameplateIndex, finalPosition, dt); + finalPosition = SnapToPixels(finalPosition, dpiScale); + + // prepare label info var pivot = (currentConfig.LightfinderAutoAlign || currentConfig.LightfinderLabelUseIcon) ? AlignmentToPivot(alignment) - : DefaultPivot; + : _defaultPivot; var textColorPacked = PackColor(labelColor); var edgeColorPacked = PackColor(edgeColor); @@ -418,11 +498,42 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe } } + /// + /// On each tick, process any needed updates for the UI Builder. + /// private void OnUiBuilderDraw() { if (!_mEnabled) return; + var fw = Framework.Instance(); + if (fw == null) + return; + + // Frame skip check + var frame = fw->FrameCounter; + + if (_lastNamePlateDrawFrame == 0 || (frame - _lastNamePlateDrawFrame) > 1) + { + ClearLabelBuffer(); + Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length); + + 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; + int copyCount; lock (_labelLock) { @@ -433,21 +544,84 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe Array.Copy(_buffers.LabelRender, _buffers.LabelCopy, copyCount); } + var uiModule = fw != null ? fw->GetUIModule() : null; + + if (uiModule != null) + { + var rapture = uiModule->GetRaptureAtkModule(); + if (rapture != null) + RefreshUiRects(&rapture->RaptureAtkUnitManager); + else + _uiRects.Clear(); + } + else + { + _uiRects.Clear(); + } + + // Needed for imgui overlay viewport for the multi window view. + var vp = ImGui.GetMainViewport(); + var vpPos = vp.Pos; + + ImGuiHelpers.ForceNextWindowMainViewport(); + + ImGui.SetNextWindowPos(vp.Pos); + ImGui.SetNextWindowSize(vp.Size); + + ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0); + ImGui.PushStyleVar(ImGuiStyleVar.WindowBorderSize, 0); + + ImGui.Begin("##LightFinderOverlay", _overlayFlags); + + ImGui.PopStyleVar(2); + using var drawList = PictoService.Draw(); if (drawList == null) + { + 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(); + } - drawList.AddScreenText(info.ScreenPosition, info.Text, info.TextColor, info.FontSize, info.Pivot, info.EdgeColor, font); + 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); } } @@ -460,15 +634,15 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe AlignmentType.Top => new Vector2(0.5f, 0f), AlignmentType.Left => new Vector2(0f, 0.5f), AlignmentType.Right => new Vector2(1f, 0.5f), - _ => DefaultPivot + _ => _defaultPivot }; 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); } @@ -514,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, @@ -545,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; @@ -554,10 +737,193 @@ 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 + var x = MathF.Round(p.X * dpiScale) / dpiScale; + var y = MathF.Round(p.Y * dpiScale) / dpiScale; + 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 + if (!_buffers.HasSmoothed[idx]) + { + _buffers.HasSmoothed[idx] = true; + _buffers.SmoothedPos[idx] = target; + 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) + { + // 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; + + // Local scale + float sx = root->ScaleX; if (sx <= 0f) sx = 1f; + float sy = root->ScaleY; if (sy <= 0f) sy = 1f; + + // 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; + + // 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) + return false; + + // Drop offscreen rects + if (l < -screen.X || t < -screen.Y || r > screen.X * 2f || b > screen.Y * 2f) + return false; + + rect = new RectF(l, t, r, b); + return true; + } + + /// + /// Refreshes the cached UI rects for occlusion checking. + /// + /// Unit Manager + private void RefreshUiRects(RaptureAtkUnitManager* unitMgr) + { + _uiRects.Clear(); + if (unitMgr == null) + return; + + var screen = ImGui.GetIO().DisplaySize; + + ref var list = ref unitMgr->AllLoadedUnitsList; + var count = (int)list.Count; + + for (int i = 0; i < count; i++) + { + var addon = list.Entries[i].Value; + if (addon == null) + continue; + + if (_mpNameplateAddon != null && addon == (AtkUnitBase*)_mpNameplateAddon) + continue; + + if (TryGetAddonRect(addon, screen, out var r)) + _uiRects.Add(r); + } + } + + /// + /// 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++) + { + if (_uiRects[i].Intersects(labelRect)) + return true; + } + 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)) - return DefaultIconGlyph; + return _defaultIconGlyph; var trimmed = rawInput.Trim(); @@ -575,17 +941,36 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe if (enumerator.MoveNext()) return enumerator.Current.ToString(); - return DefaultIconGlyph; + return _defaultIconGlyph; } + /// + /// Is the nameplate addon visible? + /// + /// Is it visible? + private bool IsNamePlateAddonVisible() + { + if (_mpNameplateAddon == null) + return false; + + var root = _mpNameplateAddon->AtkUnitBase.RootNode; + 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); var runeEnumerator = normalized.EnumerateRunes(); return runeEnumerator.MoveNext() ? runeEnumerator.Current.Value.ToString("X4", CultureInfo.InvariantCulture) - : DefaultIconGlyph; + : _defaultIconGlyph; } + private readonly struct NameplateLabelInfo { public NameplateLabelInfo( @@ -615,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) @@ -634,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); @@ -646,10 +1038,16 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe FlagRefresh(); } + /// + /// Clears all nameplate related caches. + /// public void ClearNameplateCaches() { _buffers.Clear(); ClearLabelBuffer(); + + Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length); + Array.Clear(_buffers.SmoothedPos, 0, _buffers.SmoothedPos.Length); } private sealed class NameplateBuffers @@ -668,6 +1066,10 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe public NameplateLabelInfo[] LabelRender { get; } = new NameplateLabelInfo[AddonNamePlate.NumNamePlateObjects]; 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); @@ -677,16 +1079,38 @@ 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 + { + public readonly float L, T, R, B; + public RectF(float l, float t, float r, float b) { L = l; T = t; R = r; B = b; } + + public bool Intersects(in RectF o) => + !(R <= o.L || o.R <= L || B <= o.T || o.B <= T); + } } \ No newline at end of file diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index c30d5fa..548fc75 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -19,6 +19,7 @@ using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; using LightlessSync.Services.ActorTracking; using LightlessSync.Services.Events; +using LightlessSync.Services.LightFinder; using LightlessSync.Services.Mediator; using LightlessSync.Services.PairProcessing; using LightlessSync.Services.ServerConfiguration; @@ -82,6 +83,9 @@ public class SettingsUi : WindowMediatorSubscriberBase private bool _pairDebugVisibleOnly = true; private bool _pairDiagnosticsEnabled; private string? _selectedPairDebugUid = null; + private string _lightfinderIconInput = string.Empty; + private bool _lightfinderIconInputInitialized = false; + private int _lightfinderIconPresetIndex = -1; private static readonly LightlessConfig DefaultConfig = new(); private static readonly JsonSerializerOptions DebugJsonOptions = new() { WriteIndented = true }; private MainSettingsTab _selectedMainTab = MainSettingsTab.General; @@ -122,6 +126,15 @@ public class SettingsUi : WindowMediatorSubscriberBase private const float GeneralTreeHighlightDuration = 1.5f; private readonly SeluneBrush _generalSeluneBrush = new(); + private static readonly (string Label, SeIconChar Icon)[] LightfinderIconPresets = new[] + { + ("Link Marker", SeIconChar.LinkMarker), ("Hyadelyn", SeIconChar.Hyadelyn), ("Gil", SeIconChar.Gil), + ("Quest Sync", SeIconChar.QuestSync), ("Glamoured", SeIconChar.Glamoured), + ("Glamoured (Dyed)", SeIconChar.GlamouredDyed), ("Auto-Translate Open", SeIconChar.AutoTranslateOpen), + ("Auto-Translate Close", SeIconChar.AutoTranslateClose), ("Boxed Star", SeIconChar.BoxedStar), + ("Boxed Plus", SeIconChar.BoxedPlus) + }; + private enum MainSettingsTab { General, @@ -1915,7 +1928,10 @@ public class SettingsUi : WindowMediatorSubscriberBase UiSharedService.TextWrapped( $"Storage validation is running: {_currentProgress.Item1}/{_currentProgress.Item2}"); - UiSharedService.TextWrapped($"Current item: {_currentProgress.Item3.ResolvedFilepath}"); + if (_currentProgress.Item3 != null) + { + UiSharedService.TextWrapped($"Current item: {_currentProgress.Item3.ResolvedFilepath}"); + } } } } @@ -2177,273 +2193,111 @@ public class SettingsUi : WindowMediatorSubscriberBase bool autoEnable = _configService.Current.LightfinderAutoEnableOnConnect; var autoAlign = _configService.Current.LightfinderAutoAlign; var offsetX = (int)_configService.Current.LightfinderLabelOffsetX; - var offsetY = (int)_configService.Current.LightfinderLabelOffsetY; - var labelScale = _configService.Current.LightfinderLabelScale; - bool showLightfinderInDtr = _configService.Current.ShowLightfinderInDtr; - var dtrLightfinderEnabled = SwapColorChannels(_configService.Current.DtrColorsLightfinderEnabled); - var dtrLightfinderDisabled = SwapColorChannels(_configService.Current.DtrColorsLightfinderDisabled); - var dtrLightfinderCooldown = SwapColorChannels(_configService.Current.DtrColorsLightfinderCooldown); - var dtrLightfinderUnavailable = SwapColorChannels(_configService.Current.DtrColorsLightfinderUnavailable); + var offsetY = (int)_configService.Current.LightfinderLabelOffsetY; + var labelScale = _configService.Current.LightfinderLabelScale; + bool showLightfinderInDtr = _configService.Current.ShowLightfinderInDtr; + var dtrLightfinderEnabled = SwapColorChannels(_configService.Current.DtrColorsLightfinderEnabled); + var dtrLightfinderDisabled = SwapColorChannels(_configService.Current.DtrColorsLightfinderDisabled); + var dtrLightfinderCooldown = SwapColorChannels(_configService.Current.DtrColorsLightfinderCooldown); + var dtrLightfinderUnavailable = SwapColorChannels(_configService.Current.DtrColorsLightfinderUnavailable); - ImGui.TextUnformatted("Connection"); - if (ImGui.Checkbox("Auto-enable Lightfinder on server connection", ref autoEnable)) - { - _configService.Current.LightfinderAutoEnableOnConnect = autoEnable; - _configService.Save(); - } - _uiShared.DrawHelpText("When enabled, Lightfinder will automatically turn on after reconnecting to the Lightless server."); - - UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); - - ImGui.TextUnformatted("Lightfinder Nameplate Colors"); - if (ImGui.BeginTable("##LightfinderColorTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit)) - { - ImGui.TableSetupColumn("Color", ImGuiTableColumnFlags.WidthFixed); - ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("Reset", ImGuiTableColumnFlags.WidthFixed, 40f); - ImGui.TableHeadersRow(); - - var lightfinderColors = new (string Key, string Label, string Description)[] + ImGui.TextUnformatted("Connection"); + if (ImGui.Checkbox("Auto-enable Lightfinder on server connection", ref autoEnable)) { + _configService.Current.LightfinderAutoEnableOnConnect = autoEnable; + _configService.Save(); + } + _uiShared.DrawHelpText("When enabled, Lightfinder will automatically turn on after reconnecting to the Lightless server."); + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + + ImGui.TextUnformatted("Lightfinder Nameplate Colors"); + if (ImGui.BeginTable("##LightfinderColorTable", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit)) + { + ImGui.TableSetupColumn("Color", ImGuiTableColumnFlags.WidthFixed); + ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Reset", ImGuiTableColumnFlags.WidthFixed, 40f); + ImGui.TableHeadersRow(); + + var lightfinderColors = new (string Key, string Label, string Description)[] + { ("Lightfinder", "Nameplate Text", "Color used for Lightfinder nameplate text."), ("LightfinderEdge", "Nameplate Outline", "Outline color applied around Lightfinder nameplate text.") - }; - - foreach (var (key, label, description) in lightfinderColors) - { - ImGui.TableNextRow(); - - ImGui.TableSetColumnIndex(0); - var colorValue = UIColors.Get(key); - if (ImGui.ColorEdit4($"##color_{key}", ref colorValue, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf)) - { - UIColors.Set(key, colorValue); - } - ImGui.SameLine(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(label); - - ImGui.TableSetColumnIndex(1); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(description); - - ImGui.TableSetColumnIndex(2); - using var resetId = ImRaii.PushId($"Reset_{key}"); - var availableWidth = ImGui.GetContentRegionAvail().X; - var isCustom = UIColors.IsCustom(key); - using (ImRaii.Disabled(!isCustom)) - { - using (ImRaii.PushFont(UiBuilder.IconFont)) - { - if (ImGui.Button(FontAwesomeIcon.Undo.ToIconString(), new Vector2(availableWidth, 0))) - { - UIColors.Reset(key); - } - } - } - UiSharedService.AttachToolTip(isCustom ? "Reset this color to default" : "Color is already at default value"); - } - - ImGui.EndTable(); - } - - ImGui.Spacing(); - - UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); - - ImGui.TextUnformatted("Lightfinder Info Bar"); - if (ImGui.Checkbox("Show Lightfinder status in Server info bar", ref showLightfinderInDtr)) - { - _configService.Current.ShowLightfinderInDtr = showLightfinderInDtr; - _configService.Save(); - } - _uiShared.DrawHelpText("Adds a Lightfinder status to the Server info bar. Left click toggles Lightfinder when visible."); - - var lightfinderDisplayMode = _configService.Current.LightfinderDtrDisplayMode; - var lightfinderDisplayLabel = lightfinderDisplayMode switch - { - LightfinderDtrDisplayMode.PendingPairRequests => "Pending pair requests", - _ => "Nearby Lightfinder users", - }; - - ImGui.BeginDisabled(!showLightfinderInDtr); - if (ImGui.BeginCombo("Info display", lightfinderDisplayLabel)) - { - foreach (var option in Enum.GetValues()) - { - var optionLabel = option switch - { - LightfinderDtrDisplayMode.PendingPairRequests => "Pending pair requests", - _ => "Nearby Lightfinder users", }; - var selected = option == lightfinderDisplayMode; - if (ImGui.Selectable(optionLabel, selected)) + foreach (var (key, label, description) in lightfinderColors) { - _configService.Current.LightfinderDtrDisplayMode = option; - _configService.Save(); + ImGui.TableNextRow(); + + ImGui.TableSetColumnIndex(0); + var colorValue = UIColors.Get(key); + if (ImGui.ColorEdit4($"##color_{key}", ref colorValue, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf)) + { + UIColors.Set(key, colorValue); + } + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(label); + + ImGui.TableSetColumnIndex(1); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(description); + + ImGui.TableSetColumnIndex(2); + using var resetId = ImRaii.PushId($"Reset_{key}"); + var availableWidth = ImGui.GetContentRegionAvail().X; + var isCustom = UIColors.IsCustom(key); + using (ImRaii.Disabled(!isCustom)) + { + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + if (ImGui.Button(FontAwesomeIcon.Undo.ToIconString(), new Vector2(availableWidth, 0))) + { + UIColors.Reset(key); + } + } + } + UiSharedService.AttachToolTip(isCustom ? "Reset this color to default" : "Color is already at default value"); } - if (selected) - ImGui.SetItemDefaultFocus(); + ImGui.EndTable(); } - ImGui.EndCombo(); - } - ImGui.EndDisabled(); - _uiShared.DrawHelpText("Choose what the Lightfinder info bar displays while Lightfinder is active."); + ImGui.Spacing(); - bool useLightfinderColors = _configService.Current.UseLightfinderColorsInDtr; - if (ImGui.Checkbox("Color-code the Lightfinder info bar according to status", ref useLightfinderColors)) - { - _configService.Current.UseLightfinderColorsInDtr = useLightfinderColors; - _configService.Save(); - } + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); - ImGui.BeginDisabled(!showLightfinderInDtr || !useLightfinderColors); - const ImGuiTableFlags lightfinderInfoTableFlags = ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit; - if (ImGui.BeginTable("##LightfinderInfoBarColorTable", 3, lightfinderInfoTableFlags)) - { - ImGui.TableSetupColumn("Status", ImGuiTableColumnFlags.WidthFixed, 220f); - ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("Reset", ImGuiTableColumnFlags.WidthFixed, 40f); - ImGui.TableHeadersRow(); - - DrawDtrColorRow( - "enabled", - "Enabled", - "Displayed when Lightfinder is active.", - ref dtrLightfinderEnabled, - SwapColorChannels(DefaultConfig.DtrColorsLightfinderEnabled), - value => _configService.Current.DtrColorsLightfinderEnabled = SwapColorChannels(value)); - - DrawDtrColorRow( - "disabled", - "Disabled", - "Shown when Lightfinder is turned off.", - ref dtrLightfinderDisabled, - SwapColorChannels(DefaultConfig.DtrColorsLightfinderDisabled), - value => _configService.Current.DtrColorsLightfinderDisabled = SwapColorChannels(value)); - - DrawDtrColorRow( - "cooldown", - "Cooldown", - "Displayed while Lightfinder is on cooldown.", - ref dtrLightfinderCooldown, - SwapColorChannels(DefaultConfig.DtrColorsLightfinderCooldown), - value => _configService.Current.DtrColorsLightfinderCooldown = SwapColorChannels(value)); - - DrawDtrColorRow( - "unavailable", - "Unavailable", - "Used when Lightfinder is not available on the current server.", - ref dtrLightfinderUnavailable, - SwapColorChannels(DefaultConfig.DtrColorsLightfinderUnavailable), - value => _configService.Current.DtrColorsLightfinderUnavailable = SwapColorChannels(value)); - - ImGui.EndTable(); - } - ImGui.EndDisabled(); - - UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); - - ImGui.TextUnformatted("Alignment"); - ImGui.BeginDisabled(autoAlign); - if (ImGui.SliderInt("Label Offset X", ref offsetX, -200, 200)) - { - _configService.Current.LightfinderLabelOffsetX = (short)offsetX; - _configService.Save(); - _nameplateService.RequestRedraw(); - } - - if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) - { - _configService.Current.LightfinderLabelOffsetX = 0; - _configService.Save(); - _nameplateService.RequestRedraw(); - } - - if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Right click to reset to default."); - ImGui.EndDisabled(); - _uiShared.DrawHelpText( - "Moves the Lightfinder label horizontally on player nameplates.\nUnavailable when automatic alignment is enabled."); - - - if (ImGui.SliderInt("Label Offset Y", ref offsetY, -200, 200)) - { - _configService.Current.LightfinderLabelOffsetY = (short)offsetY; - _configService.Save(); - _nameplateService.RequestRedraw(); - } - - if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) - { - _configService.Current.LightfinderLabelOffsetY = 0; - _configService.Save(); - _nameplateService.RequestRedraw(); - } - - if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Right click to reset to default."); - _uiShared.DrawHelpText("Moves the Lightfinder label vertically on player nameplates."); - - if (ImGui.SliderFloat("Label Size", ref labelScale, 0.5f, 2.0f, "%.2fx")) - { - _configService.Current.LightfinderLabelScale = labelScale; - _configService.Save(); - _nameplateService.RequestRedraw(); - } - - if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) - { - _configService.Current.LightfinderLabelScale = 1.0f; - _configService.Save(); - _nameplateService.RequestRedraw(); - } - - if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Right click to reset to default."); - _uiShared.DrawHelpText("Adjusts the Lightfinder label size for both text and icon modes."); - - ImGui.Dummy(new Vector2(8)); - - if (ImGui.Checkbox("Automatically align with nameplate", ref autoAlign)) - { - _configService.Current.LightfinderAutoAlign = autoAlign; - _configService.Save(); - _nameplateService.RequestRedraw(); - } - - _uiShared.DrawHelpText( - "Automatically position the label relative to the in-game nameplate. Turn off to rely entirely on manual offsets."); - - if (autoAlign) - { - var alignmentOption = _configService.Current.LabelAlignment; - var alignmentLabel = alignmentOption switch + ImGui.TextUnformatted("Lightfinder Info Bar"); + if (ImGui.Checkbox("Show Lightfinder status in Server info bar", ref showLightfinderInDtr)) { - LabelAlignment.Left => "Left", - LabelAlignment.Right => "Right", - _ => "Center", + _configService.Current.ShowLightfinderInDtr = showLightfinderInDtr; + _configService.Save(); + } + _uiShared.DrawHelpText("Adds a Lightfinder status to the Server info bar. Left click toggles Lightfinder when visible."); + + var lightfinderDisplayMode = _configService.Current.LightfinderDtrDisplayMode; + var lightfinderDisplayLabel = lightfinderDisplayMode switch + { + LightfinderDtrDisplayMode.PendingPairRequests => "Pending pair requests", + _ => "Nearby Lightfinder users", }; - if (ImGui.BeginCombo("Horizontal Alignment", alignmentLabel)) + ImGui.BeginDisabled(!showLightfinderInDtr); + if (ImGui.BeginCombo("Info display", lightfinderDisplayLabel)) { - foreach (LabelAlignment option in Enum.GetValues()) + foreach (var option in Enum.GetValues()) { var optionLabel = option switch { - LabelAlignment.Left => "Left", - LabelAlignment.Right => "Right", - _ => "Center", + LightfinderDtrDisplayMode.PendingPairRequests => "Pending pair requests", + _ => "Nearby Lightfinder users", }; - var selected = option == alignmentOption; + + var selected = option == lightfinderDisplayMode; if (ImGui.Selectable(optionLabel, selected)) { - _configService.Current.LabelAlignment = option; + _configService.Current.LightfinderDtrDisplayMode = option; _configService.Save(); - _nameplateService.RequestRedraw(); } if (selected) @@ -2452,239 +2306,480 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.EndCombo(); } + ImGui.EndDisabled(); + _uiShared.DrawHelpText("Choose what the Lightfinder info bar displays while Lightfinder is active."); - } + bool useLightfinderColors = _configService.Current.UseLightfinderColorsInDtr; + if (ImGui.Checkbox("Color-code the Lightfinder info bar according to status", ref useLightfinderColors)) + { + _configService.Current.UseLightfinderColorsInDtr = useLightfinderColors; + _configService.Save(); + } - UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + ImGui.BeginDisabled(!showLightfinderInDtr || !useLightfinderColors); + const ImGuiTableFlags lightfinderInfoTableFlags = ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit; + if (ImGui.BeginTable("##LightfinderInfoBarColorTable", 3, lightfinderInfoTableFlags)) + { + ImGui.TableSetupColumn("Status", ImGuiTableColumnFlags.WidthFixed, 220f); + ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Reset", ImGuiTableColumnFlags.WidthFixed, 40f); + ImGui.TableHeadersRow(); - ImGui.TextUnformatted("Visibility"); - var showOwn = _configService.Current.LightfinderLabelShowOwn; - if (ImGui.Checkbox("Show your own Lightfinder label", ref showOwn)) - { - _configService.Current.LightfinderLabelShowOwn = showOwn; - _configService.Save(); - _nameplateService.RequestRedraw(); - } + DrawDtrColorRow( + "enabled", + "Enabled", + "Displayed when Lightfinder is active.", + ref dtrLightfinderEnabled, + SwapColorChannels(DefaultConfig.DtrColorsLightfinderEnabled), + value => _configService.Current.DtrColorsLightfinderEnabled = SwapColorChannels(value)); - _uiShared.DrawHelpText("Toggles your own Lightfinder label."); + DrawDtrColorRow( + "disabled", + "Disabled", + "Shown when Lightfinder is turned off.", + ref dtrLightfinderDisabled, + SwapColorChannels(DefaultConfig.DtrColorsLightfinderDisabled), + value => _configService.Current.DtrColorsLightfinderDisabled = SwapColorChannels(value)); - var showPaired = _configService.Current.LightfinderLabelShowPaired; - if (ImGui.Checkbox("Show paired player(s) Lightfinder label", ref showPaired)) - { - _configService.Current.LightfinderLabelShowPaired = showPaired; - _configService.Save(); - _nameplateService.RequestRedraw(); - } + DrawDtrColorRow( + "cooldown", + "Cooldown", + "Displayed while Lightfinder is on cooldown.", + ref dtrLightfinderCooldown, + SwapColorChannels(DefaultConfig.DtrColorsLightfinderCooldown), + value => _configService.Current.DtrColorsLightfinderCooldown = SwapColorChannels(value)); - _uiShared.DrawHelpText("Toggles paired player(s) Lightfinder label."); + DrawDtrColorRow( + "unavailable", + "Unavailable", + "Used when Lightfinder is not available on the current server.", + ref dtrLightfinderUnavailable, + SwapColorChannels(DefaultConfig.DtrColorsLightfinderUnavailable), + value => _configService.Current.DtrColorsLightfinderUnavailable = SwapColorChannels(value)); - var showHidden = _configService.Current.LightfinderLabelShowHidden; - if (ImGui.Checkbox("Show Lightfinder label when no nameplate(s) is visible", ref showHidden)) - { - _configService.Current.LightfinderLabelShowHidden = showHidden; - _configService.Save(); - _nameplateService.RequestRedraw(); - } - _uiShared.DrawHelpText("Toggles Lightfinder label when no nameplate(s) is visible."); + ImGui.EndTable(); + } + ImGui.EndDisabled(); - UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); - ImGui.TextUnformatted("Label"); - var useIcon = _configService.Current.LightfinderLabelUseIcon; - if (ImGui.Checkbox("Show icon instead of text", ref useIcon)) - { - _configService.Current.LightfinderLabelUseIcon = useIcon; - _configService.Save(); - _nameplateService.RequestRedraw(); + ImGui.TextUnformatted("Alignment"); + ImGui.BeginDisabled(autoAlign); + if (ImGui.SliderInt("Label Offset X", ref offsetX, -200, 200)) + { + _configService.Current.LightfinderLabelOffsetX = (short)offsetX; + _configService.Save(); + _nameplateService.RequestRedraw(); + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _configService.Current.LightfinderLabelOffsetX = 0; + _configService.Save(); + _nameplateService.RequestRedraw(); + } + + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Right click to reset to default."); + ImGui.EndDisabled(); + _uiShared.DrawHelpText( + "Moves the Lightfinder label horizontally on player nameplates.\nUnavailable when automatic alignment is enabled."); + + + if (ImGui.SliderInt("Label Offset Y", ref offsetY, -200, 200)) + { + _configService.Current.LightfinderLabelOffsetY = (short)offsetY; + _configService.Save(); + _nameplateService.RequestRedraw(); + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _configService.Current.LightfinderLabelOffsetY = 0; + _configService.Save(); + _nameplateService.RequestRedraw(); + } + + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Right click to reset to default."); + _uiShared.DrawHelpText("Moves the Lightfinder label vertically on player nameplates."); + + if (ImGui.SliderFloat("Label Size", ref labelScale, 0.5f, 2.0f, "%.2fx")) + { + _configService.Current.LightfinderLabelScale = labelScale; + _configService.Save(); + _nameplateService.RequestRedraw(); + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + { + _configService.Current.LightfinderLabelScale = 1.0f; + _configService.Save(); + _nameplateService.RequestRedraw(); + } + + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("Right click to reset to default."); + _uiShared.DrawHelpText("Adjusts the Lightfinder label size for both text and icon modes."); + + ImGui.Dummy(new Vector2(8)); + + if (ImGui.Checkbox("Automatically align with nameplate", ref autoAlign)) + { + _configService.Current.LightfinderAutoAlign = autoAlign; + _configService.Save(); + _nameplateService.RequestRedraw(); + } + + _uiShared.DrawHelpText( + "Automatically position the label relative to the in-game nameplate. Turn off to rely entirely on manual offsets."); + + if (autoAlign) + { + var alignmentOption = _configService.Current.LabelAlignment; + var alignmentLabel = alignmentOption switch + { + LabelAlignment.Left => "Left", + LabelAlignment.Right => "Right", + _ => "Center", + }; + + if (ImGui.BeginCombo("Horizontal Alignment", alignmentLabel)) + { + foreach (LabelAlignment option in Enum.GetValues()) + { + var optionLabel = option switch + { + LabelAlignment.Left => "Left", + LabelAlignment.Right => "Right", + _ => "Center", + }; + var selected = option == alignmentOption; + if (ImGui.Selectable(optionLabel, selected)) + { + _configService.Current.LabelAlignment = option; + _configService.Save(); + _nameplateService.RequestRedraw(); + } + + if (selected) + ImGui.SetItemDefaultFocus(); + } + + ImGui.EndCombo(); + } + + } + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + + ImGui.TextUnformatted("Visibility"); + var showOwn = _configService.Current.LightfinderLabelShowOwn; + if (ImGui.Checkbox("Show your own Lightfinder label", ref showOwn)) + { + _configService.Current.LightfinderLabelShowOwn = showOwn; + _configService.Save(); + _nameplateService.RequestRedraw(); + } + + _uiShared.DrawHelpText("Toggles your own Lightfinder label."); + + var showPaired = _configService.Current.LightfinderLabelShowPaired; + if (ImGui.Checkbox("Show paired player(s) Lightfinder label", ref showPaired)) + { + _configService.Current.LightfinderLabelShowPaired = showPaired; + _configService.Save(); + _nameplateService.RequestRedraw(); + } + + _uiShared.DrawHelpText("Toggles paired player(s) Lightfinder label."); + + var showHidden = _configService.Current.LightfinderLabelShowHidden; + if (ImGui.Checkbox("Show Lightfinder label when no nameplate(s) is visible", ref showHidden)) + { + _configService.Current.LightfinderLabelShowHidden = showHidden; + _configService.Save(); + _nameplateService.RequestRedraw(); + } + _uiShared.DrawHelpText("Toggles Lightfinder label when no nameplate(s) is visible."); + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + + ImGui.TextUnformatted("Label"); + var useIcon = _configService.Current.LightfinderLabelUseIcon; + if (ImGui.Checkbox("Show icon instead of text", ref useIcon)) + { + _configService.Current.LightfinderLabelUseIcon = useIcon; + _configService.Save(); + + if (useIcon) + { + RefreshLightfinderIconState(); + } + else + { + _lightfinderIconInputInitialized = false; + _lightfinderIconPresetIndex = -1; + } + } + + _uiShared.DrawHelpText("Switch between the Lightfinder text label and an icon on nameplates."); if (useIcon) { - // redo - } - } - - _uiShared.DrawHelpText("Switch between the Lightfinder text label and an icon on nameplates."); - - if (useIcon) - { - //redo - } - - UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); - ImGui.TreePop(); - lightfinderTree.MarkContentEnd(); - } - } - - ImGui.Separator(); - - using (var pairListTree = BeginGeneralTree("Pair List", UIColors.Get("LightlessPurple"))) - { - if (pairListTree.Visible) - { - if (ImGui.Checkbox("Show separate Visible group", ref showVisibleSeparate)) - { - _configService.Current.ShowVisibleUsersSeparately = showVisibleSeparate; - _configService.Save(); - Mediator.Publish(new RefreshUiMessage()); - } - - _uiShared.DrawHelpText( - "This will show all currently visible users in a special 'Visible' group in the main UI."); - - using (ImRaii.Disabled(!showVisibleSeparate)) - { - using var indent = ImRaii.PushIndent(); - if (ImGui.Checkbox("Show Syncshell Users in Visible Group", ref groupInVisible)) + if (!_lightfinderIconInputInitialized) { - _configService.Current.ShowSyncshellUsersInVisible = groupInVisible; - _configService.Save(); - Mediator.Publish(new RefreshUiMessage()); + RefreshLightfinderIconState(); } - } - if (ImGui.Checkbox("Show separate Offline group", ref showOfflineSeparate)) - { - _configService.Current.ShowOfflineUsersSeparately = showOfflineSeparate; - _configService.Save(); - Mediator.Publish(new RefreshUiMessage()); - } + var currentPresetLabel = _lightfinderIconPresetIndex >= 0 + ? $"{GetLightfinderPresetGlyph(_lightfinderIconPresetIndex)} {LightfinderIconPresets[_lightfinderIconPresetIndex].Label}" + : "Custom"; - _uiShared.DrawHelpText( - "This will show all currently offline users in a special 'Offline' group in the main UI."); - - using (ImRaii.Disabled(!showOfflineSeparate)) - { - using var indent = ImRaii.PushIndent(); - if (ImGui.Checkbox("Show separate Offline group for Syncshell users", ref syncshellOfflineSeparate)) + if (ImGui.BeginCombo("Preset Icon", currentPresetLabel)) { - _configService.Current.ShowSyncshellOfflineUsersSeparately = syncshellOfflineSeparate; - _configService.Save(); - Mediator.Publish(new RefreshUiMessage()); + for (int i = 0; i < LightfinderIconPresets.Length; i++) + { + var optionGlyph = GetLightfinderPresetGlyph(i); + var preview = $"{optionGlyph} {LightfinderIconPresets[i].Label}"; + var selected = i == _lightfinderIconPresetIndex; + if (ImGui.Selectable(preview, selected)) + { + _lightfinderIconInput = LightFinderPlateHandler.ToIconEditorString(optionGlyph); + _lightfinderIconPresetIndex = i; + } + } + + if (ImGui.Selectable("Custom", _lightfinderIconPresetIndex == -1)) + { + _lightfinderIconPresetIndex = -1; + } + + ImGui.EndCombo(); } + + var editorBuffer = _lightfinderIconInput; + if (ImGui.InputText("Icon Glyph", ref editorBuffer, 16)) + { + _lightfinderIconInput = editorBuffer; + _lightfinderIconPresetIndex = -1; + } + + if (ImGui.Button("Apply Icon")) + { + var normalized = LightFinderPlateHandler.NormalizeIconGlyph(_lightfinderIconInput); + ApplyLightfinderIcon(normalized, _lightfinderIconPresetIndex); + } + + ImGui.SameLine(); + if (ImGui.Button("Reset Icon")) + { + var defaultGlyph = LightFinderPlateHandler.NormalizeIconGlyph(null); + var defaultIndex = -1; + for (int i = 0; i < LightfinderIconPresets.Length; i++) + { + if (string.Equals(GetLightfinderPresetGlyph(i), defaultGlyph, StringComparison.Ordinal)) + { + defaultIndex = i; + break; + } + } + + if (defaultIndex < 0) + { + defaultIndex = 0; + } + + ApplyLightfinderIcon(GetLightfinderPresetGlyph(defaultIndex), defaultIndex); + } + + var previewGlyph = LightFinderPlateHandler.NormalizeIconGlyph(_lightfinderIconInput); + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.Text($"Preview: {previewGlyph}"); + _uiShared.DrawHelpText( + "Enter a hex code (e.g. E0BB), pick a preset, or paste an icon character directly."); } - - if (ImGui.Checkbox("Group up all syncshells in one folder", ref groupUpSyncshells)) + else { - _configService.Current.GroupUpSyncshells = groupUpSyncshells; - _configService.Save(); - Mediator.Publish(new RefreshUiMessage()); - } - - _uiShared.DrawHelpText( - "This will group up all Syncshells in a special 'All Syncshells' folder in the main UI."); - - if (ImGui.Checkbox("Show grouped syncshells in main screen/all syncshells", ref groupedSyncshells)) - { - _configService.Current.ShowGroupedSyncshellsInAll = groupedSyncshells; - _configService.Save(); - Mediator.Publish(new RefreshUiMessage()); - } - - _uiShared.DrawHelpText("This will show grouped syncshells in main screen or group 'All Syncshells'."); - - if (ImGui.Checkbox("Show player name for visible players", ref showNameInsteadOfNotes)) - { - _configService.Current.ShowCharacterNameInsteadOfNotesForVisible = showNameInsteadOfNotes; - _configService.Save(); - Mediator.Publish(new RefreshUiMessage()); - } - - _uiShared.DrawHelpText( - "This will show the character name instead of custom set note when a character is visible"); - - ImGui.Indent(); - if (!_configService.Current.ShowCharacterNameInsteadOfNotesForVisible) ImGui.BeginDisabled(); - if (ImGui.Checkbox("Prefer notes over player names for visible players", ref preferNotesInsteadOfName)) - { - _configService.Current.PreferNotesOverNamesForVisible = preferNotesInsteadOfName; - _configService.Save(); - Mediator.Publish(new RefreshUiMessage()); - } - - _uiShared.DrawHelpText("If you set a note for a player it will be shown instead of the player name"); - if (!_configService.Current.ShowCharacterNameInsteadOfNotesForVisible) ImGui.EndDisabled(); - ImGui.Unindent(); - - if (ImGui.Checkbox("Set visible pairs as focus targets when clicking the eye", ref useFocusTarget)) - { - _configService.Current.UseFocusTarget = useFocusTarget; - _configService.Save(); - } - - if (ImGui.Checkbox("Set visible pair icon to an green color", ref greenVisiblePair)) - { - _configService.Current.ShowVisiblePairsGreenEye = greenVisiblePair; - _configService.Save(); + _lightfinderIconInputInitialized = false; + _lightfinderIconPresetIndex = -1; } UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); ImGui.TreePop(); - pairListTree.MarkContentEnd(); + lightfinderTree.MarkContentEnd(); } - } - ImGui.Separator(); + ImGui.Separator(); - using (var profilesTree = BeginGeneralTree("Profiles", UIColors.Get("LightlessPurple"))) - { - if (profilesTree.Visible) + using (var pairListTree = BeginGeneralTree("Pair List", UIColors.Get("LightlessPurple"))) { - if (ImGui.Checkbox("Show Lightless Profiles on Hover", ref showProfiles)) + if (pairListTree.Visible) { - Mediator.Publish(new ClearProfileUserDataMessage()); - _configService.Current.ProfilesShow = showProfiles; - _configService.Save(); + if (ImGui.Checkbox("Show separate Visible group", ref showVisibleSeparate)) + { + _configService.Current.ShowVisibleUsersSeparately = showVisibleSeparate; + _configService.Save(); + Mediator.Publish(new RefreshUiMessage()); + } + + _uiShared.DrawHelpText( + "This will show all currently visible users in a special 'Visible' group in the main UI."); + + using (ImRaii.Disabled(!showVisibleSeparate)) + { + using var indent = ImRaii.PushIndent(); + if (ImGui.Checkbox("Show Syncshell Users in Visible Group", ref groupInVisible)) + { + _configService.Current.ShowSyncshellUsersInVisible = groupInVisible; + _configService.Save(); + Mediator.Publish(new RefreshUiMessage()); + } + } + + if (ImGui.Checkbox("Show separate Offline group", ref showOfflineSeparate)) + { + _configService.Current.ShowOfflineUsersSeparately = showOfflineSeparate; + _configService.Save(); + Mediator.Publish(new RefreshUiMessage()); + } + + _uiShared.DrawHelpText( + "This will show all currently offline users in a special 'Offline' group in the main UI."); + + using (ImRaii.Disabled(!showOfflineSeparate)) + { + using var indent = ImRaii.PushIndent(); + if (ImGui.Checkbox("Show separate Offline group for Syncshell users", ref syncshellOfflineSeparate)) + { + _configService.Current.ShowSyncshellOfflineUsersSeparately = syncshellOfflineSeparate; + _configService.Save(); + Mediator.Publish(new RefreshUiMessage()); + } + } + + if (ImGui.Checkbox("Group up all syncshells in one folder", ref groupUpSyncshells)) + { + _configService.Current.GroupUpSyncshells = groupUpSyncshells; + _configService.Save(); + Mediator.Publish(new RefreshUiMessage()); + } + + _uiShared.DrawHelpText( + "This will group up all Syncshells in a special 'All Syncshells' folder in the main UI."); + + if (ImGui.Checkbox("Show grouped syncshells in main screen/all syncshells", ref groupedSyncshells)) + { + _configService.Current.ShowGroupedSyncshellsInAll = groupedSyncshells; + _configService.Save(); + Mediator.Publish(new RefreshUiMessage()); + } + + _uiShared.DrawHelpText("This will show grouped syncshells in main screen or group 'All Syncshells'."); + + if (ImGui.Checkbox("Show player name for visible players", ref showNameInsteadOfNotes)) + { + _configService.Current.ShowCharacterNameInsteadOfNotesForVisible = showNameInsteadOfNotes; + _configService.Save(); + Mediator.Publish(new RefreshUiMessage()); + } + + _uiShared.DrawHelpText( + "This will show the character name instead of custom set note when a character is visible"); + + ImGui.Indent(); + if (!_configService.Current.ShowCharacterNameInsteadOfNotesForVisible) ImGui.BeginDisabled(); + if (ImGui.Checkbox("Prefer notes over player names for visible players", ref preferNotesInsteadOfName)) + { + _configService.Current.PreferNotesOverNamesForVisible = preferNotesInsteadOfName; + _configService.Save(); + Mediator.Publish(new RefreshUiMessage()); + } + + _uiShared.DrawHelpText("If you set a note for a player it will be shown instead of the player name"); + if (!_configService.Current.ShowCharacterNameInsteadOfNotesForVisible) ImGui.EndDisabled(); + ImGui.Unindent(); + + if (ImGui.Checkbox("Set visible pairs as focus targets when clicking the eye", ref useFocusTarget)) + { + _configService.Current.UseFocusTarget = useFocusTarget; + _configService.Save(); + } + + if (ImGui.Checkbox("Set visible pair icon to an green color", ref greenVisiblePair)) + { + _configService.Current.ShowVisiblePairsGreenEye = greenVisiblePair; + _configService.Save(); + } + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGui.TreePop(); + pairListTree.MarkContentEnd(); } - - _uiShared.DrawHelpText("This will show the configured user profile after a set delay"); - ImGui.Indent(); - if (!showProfiles) ImGui.BeginDisabled(); - if (ImGui.Checkbox("Popout profiles on the right", ref profileOnRight)) - { - _configService.Current.ProfilePopoutRight = profileOnRight; - _configService.Save(); - Mediator.Publish(new CompactUiChange(Vector2.Zero, Vector2.Zero)); - } - - _uiShared.DrawHelpText("Will show profiles on the right side of the main UI"); - if (ImGui.SliderFloat("Hover Delay", ref profileDelay, 1, 10)) - { - _configService.Current.ProfileDelay = profileDelay; - _configService.Save(); - } - - _uiShared.DrawHelpText("Delay until the profile should be displayed"); - if (!showProfiles) ImGui.EndDisabled(); - ImGui.Unindent(); - if (ImGui.Checkbox("Show profiles marked as NSFW", ref showNsfwProfiles)) - { - Mediator.Publish(new ClearProfileUserDataMessage()); - _configService.Current.ProfilesAllowNsfw = showNsfwProfiles; - _configService.Save(); - } - - _uiShared.DrawHelpText("Will show profiles that have the NSFW tag enabled"); - - UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); - ImGui.TreePop(); - profilesTree.MarkContentEnd(); } - } - ImGui.Separator(); + ImGui.Separator(); - ImGui.Dummy(new Vector2(10)); - _uiShared.BigText("UI Theme"); - - using (var colorsTree = BeginGeneralTree("Colors", UIColors.Get("LightlessPurple"))) - { - if (colorsTree.Visible) + using (var profilesTree = BeginGeneralTree("Profiles", UIColors.Get("LightlessPurple"))) { - ImGui.TextUnformatted("UI Theme Colors"); - - var colorNames = new[] + if (profilesTree.Visible) { + if (ImGui.Checkbox("Show Lightless Profiles on Hover", ref showProfiles)) + { + Mediator.Publish(new ClearProfileUserDataMessage()); + _configService.Current.ProfilesShow = showProfiles; + _configService.Save(); + } + + _uiShared.DrawHelpText("This will show the configured user profile after a set delay"); + ImGui.Indent(); + if (!showProfiles) ImGui.BeginDisabled(); + if (ImGui.Checkbox("Popout profiles on the right", ref profileOnRight)) + { + _configService.Current.ProfilePopoutRight = profileOnRight; + _configService.Save(); + Mediator.Publish(new CompactUiChange(Vector2.Zero, Vector2.Zero)); + } + + _uiShared.DrawHelpText("Will show profiles on the right side of the main UI"); + if (ImGui.SliderFloat("Hover Delay", ref profileDelay, 1, 10)) + { + _configService.Current.ProfileDelay = profileDelay; + _configService.Save(); + } + + _uiShared.DrawHelpText("Delay until the profile should be displayed"); + if (!showProfiles) ImGui.EndDisabled(); + ImGui.Unindent(); + if (ImGui.Checkbox("Show profiles marked as NSFW", ref showNsfwProfiles)) + { + Mediator.Publish(new ClearProfileUserDataMessage()); + _configService.Current.ProfilesAllowNsfw = showNsfwProfiles; + _configService.Save(); + } + + _uiShared.DrawHelpText("Will show profiles that have the NSFW tag enabled"); + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGui.TreePop(); + profilesTree.MarkContentEnd(); + } + } + + ImGui.Separator(); + + ImGui.Dummy(new Vector2(10)); + _uiShared.BigText("UI Theme"); + + using (var colorsTree = BeginGeneralTree("Colors", UIColors.Get("LightlessPurple"))) + { + if (colorsTree.Visible) + { + ImGui.TextUnformatted("UI Theme Colors"); + + var colorNames = new[] + { ("LightlessPurple", "Primary Purple", "Section titles and dividers"), ("LightlessPurpleActive", "Primary Purple (Active)", "Active tabs and hover highlights"), ("LightlessPurpleDefault", "Primary Purple (Inactive)", "Inactive tabs and default dividers"), @@ -2695,216 +2790,217 @@ public class SettingsUi : WindowMediatorSubscriberBase ("PairBlue", "Syncshell Blue", "Syncshell headers, toggle highlights, and moderator actions"), ("DimRed", "Error Red", "Error and offline colors") }; - if (ImGui.BeginTable("##ColorTable", 3, - ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit)) - { - ImGui.TableSetupColumn("Color", ImGuiTableColumnFlags.WidthFixed); - ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("Reset", ImGuiTableColumnFlags.WidthFixed, 40); - ImGui.TableHeadersRow(); - - foreach (var (colorKey, displayName, description) in colorNames) + if (ImGui.BeginTable("##ColorTable", 3, + ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit)) { - ImGui.TableNextRow(); + ImGui.TableSetupColumn("Color", ImGuiTableColumnFlags.WidthFixed); + ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Reset", ImGuiTableColumnFlags.WidthFixed, 40); + ImGui.TableHeadersRow(); - ImGui.TableSetColumnIndex(0); - var currentColor = UIColors.Get(colorKey); - var colorToEdit = currentColor; - if (ImGui.ColorEdit4($"##color_{colorKey}", ref colorToEdit, - ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf)) + foreach (var (colorKey, displayName, description) in colorNames) { - UIColors.Set(colorKey, colorToEdit); - } + ImGui.TableNextRow(); - ImGui.SameLine(); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(displayName); - - ImGui.TableSetColumnIndex(1); - ImGui.AlignTextToFramePadding(); - ImGui.TextUnformatted(description); - - ImGui.TableSetColumnIndex(2); - using var resetId = ImRaii.PushId($"Reset_{colorKey}"); - var availableWidth = ImGui.GetContentRegionAvail().X; - var isCustom = UIColors.IsCustom(colorKey); - - using (ImRaii.Disabled(!isCustom)) - { - using (ImRaii.PushFont(UiBuilder.IconFont)) + ImGui.TableSetColumnIndex(0); + var currentColor = UIColors.Get(colorKey); + var colorToEdit = currentColor; + if (ImGui.ColorEdit4($"##color_{colorKey}", ref colorToEdit, + ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf)) { - if (ImGui.Button(FontAwesomeIcon.Undo.ToIconString(), new Vector2(availableWidth, 0))) + UIColors.Set(colorKey, colorToEdit); + } + + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(displayName); + + ImGui.TableSetColumnIndex(1); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(description); + + ImGui.TableSetColumnIndex(2); + using var resetId = ImRaii.PushId($"Reset_{colorKey}"); + var availableWidth = ImGui.GetContentRegionAvail().X; + var isCustom = UIColors.IsCustom(colorKey); + + using (ImRaii.Disabled(!isCustom)) + { + using (ImRaii.PushFont(UiBuilder.IconFont)) { - UIColors.Reset(colorKey); + if (ImGui.Button(FontAwesomeIcon.Undo.ToIconString(), new Vector2(availableWidth, 0))) + { + UIColors.Reset(colorKey); + } } } + + UiSharedService.AttachToolTip(isCustom + ? "Reset this color to default" + : "Color is already at default value"); } - UiSharedService.AttachToolTip(isCustom - ? "Reset this color to default" - : "Color is already at default value"); + ImGui.EndTable(); } - ImGui.EndTable(); + ImGui.Spacing(); + if (_uiShared.IconTextButton(FontAwesomeIcon.Undo, "Reset All Theme Colors")) + { + UIColors.ResetAll(); + } + + _uiShared.DrawHelpText("This will reset all theme colors to their default values"); + + ImGui.Spacing(); + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + + ImGui.TextUnformatted("UI Theme"); + + if (ImGui.Checkbox("Use the redesign of the UI for Lightless client", ref useLightlessRedesign)) + { + _configService.Current.UseLightlessRedesign = useLightlessRedesign; + _configService.Save(); + } + + var usePairColoredUIDs = _configService.Current.useColoredUIDs; + + if (ImGui.Checkbox("Toggle the colored UID's in pair list", ref usePairColoredUIDs)) + { + _configService.Current.useColoredUIDs = usePairColoredUIDs; + _configService.Save(); + } + + _uiShared.DrawHelpText("This changes the vanity colored UID's in pair list."); + + DrawThemeOverridesSection(); + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGui.TreePop(); + colorsTree.MarkContentEnd(); } - - ImGui.Spacing(); - if (_uiShared.IconTextButton(FontAwesomeIcon.Undo, "Reset All Theme Colors")) - { - UIColors.ResetAll(); - } - - _uiShared.DrawHelpText("This will reset all theme colors to their default values"); - - ImGui.Spacing(); - - UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); - - ImGui.TextUnformatted("UI Theme"); - - if (ImGui.Checkbox("Use the redesign of the UI for Lightless client", ref useLightlessRedesign)) - { - _configService.Current.UseLightlessRedesign = useLightlessRedesign; - _configService.Save(); - } - - var usePairColoredUIDs = _configService.Current.useColoredUIDs; - - if (ImGui.Checkbox("Toggle the colored UID's in pair list", ref usePairColoredUIDs)) - { - _configService.Current.useColoredUIDs = usePairColoredUIDs; - _configService.Save(); - } - - _uiShared.DrawHelpText("This changes the vanity colored UID's in pair list."); - - DrawThemeOverridesSection(); - - UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); - ImGui.TreePop(); - colorsTree.MarkContentEnd(); } - } - ImGui.Separator(); + ImGui.Separator(); - using (var serverInfoTree = BeginGeneralTree("Server Info Bar", UIColors.Get("LightlessPurple"))) - { - if (serverInfoTree.Visible) + using (var serverInfoTree = BeginGeneralTree("Server Info Bar", UIColors.Get("LightlessPurple"))) { - ImGui.TextUnformatted("Server Info Bar Colors"); - - if (ImGui.Checkbox("Color-code the Server Info Bar entry according to status", ref useColorsInDtr)) + if (serverInfoTree.Visible) { - _configService.Current.UseColorsInDtr = useColorsInDtr; - _configService.Save(); + ImGui.TextUnformatted("Server Info Bar Colors"); + + if (ImGui.Checkbox("Color-code the Server Info Bar entry according to status", ref useColorsInDtr)) + { + _configService.Current.UseColorsInDtr = useColorsInDtr; + _configService.Save(); + } + + _uiShared.DrawHelpText( + "This will color the Server Info Bar entry based on connection status and visible pairs."); + + ImGui.BeginDisabled(!useColorsInDtr); + const ImGuiTableFlags serverInfoTableFlags = ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit; + if (ImGui.BeginTable("##ServerInfoBarColorTable", 3, serverInfoTableFlags)) + { + ImGui.TableSetupColumn("Status", ImGuiTableColumnFlags.WidthFixed, 220f); + ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Reset", ImGuiTableColumnFlags.WidthFixed, 40f); + ImGui.TableHeadersRow(); + + DrawDtrColorRow( + "server-default", + "Default", + "Displayed when connected without any special status.", + ref dtrColorsDefault, + DefaultConfig.DtrColorsDefault, + value => _configService.Current.DtrColorsDefault = value); + + DrawDtrColorRow( + "server-not-connected", + "Not Connected", + "Shown while disconnected from the Lightless server.", + ref dtrColorsNotConnected, + DefaultConfig.DtrColorsNotConnected, + value => _configService.Current.DtrColorsNotConnected = value); + + DrawDtrColorRow( + "server-pairs", + "Pairs in Range", + "Used when nearby paired players are detected.", + ref dtrColorsPairsInRange, + DefaultConfig.DtrColorsPairsInRange, + value => _configService.Current.DtrColorsPairsInRange = value); + + ImGui.EndTable(); + } + ImGui.EndDisabled(); + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGui.TreePop(); + serverInfoTree.MarkContentEnd(); } - - _uiShared.DrawHelpText( - "This will color the Server Info Bar entry based on connection status and visible pairs."); - - ImGui.BeginDisabled(!useColorsInDtr); - const ImGuiTableFlags serverInfoTableFlags = ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit; - if (ImGui.BeginTable("##ServerInfoBarColorTable", 3, serverInfoTableFlags)) - { - ImGui.TableSetupColumn("Status", ImGuiTableColumnFlags.WidthFixed, 220f); - ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableSetupColumn("Reset", ImGuiTableColumnFlags.WidthFixed, 40f); - ImGui.TableHeadersRow(); - - DrawDtrColorRow( - "server-default", - "Default", - "Displayed when connected without any special status.", - ref dtrColorsDefault, - DefaultConfig.DtrColorsDefault, - value => _configService.Current.DtrColorsDefault = value); - - DrawDtrColorRow( - "server-not-connected", - "Not Connected", - "Shown while disconnected from the Lightless server.", - ref dtrColorsNotConnected, - DefaultConfig.DtrColorsNotConnected, - value => _configService.Current.DtrColorsNotConnected = value); - - DrawDtrColorRow( - "server-pairs", - "Pairs in Range", - "Used when nearby paired players are detected.", - ref dtrColorsPairsInRange, - DefaultConfig.DtrColorsPairsInRange, - value => _configService.Current.DtrColorsPairsInRange = value); - - ImGui.EndTable(); - } - ImGui.EndDisabled(); - - UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); - ImGui.TreePop(); - serverInfoTree.MarkContentEnd(); } - } - ImGui.Separator(); + ImGui.Separator(); - using (var nameplateTree = BeginGeneralTree("Nameplate", UIColors.Get("LightlessPurple"))) - { - if (nameplateTree.Visible) + using (var nameplateTree = BeginGeneralTree("Nameplate", UIColors.Get("LightlessPurple"))) { - ImGui.TextUnformatted("Nameplate Colors"); - - var nameColorsEnabled = _configService.Current.IsNameplateColorsEnabled; - var nameColors = _configService.Current.NameplateColors; - var isFriendOverride = _configService.Current.overrideFriendColor; - var isPartyOverride = _configService.Current.overridePartyColor; - - if (ImGui.Checkbox("Override name color of visible paired players", ref nameColorsEnabled)) + if (nameplateTree.Visible) { - _configService.Current.IsNameplateColorsEnabled = nameColorsEnabled; - _configService.Save(); - _nameplateService.RequestRedraw(); + ImGui.TextUnformatted("Nameplate Colors"); + + var nameColorsEnabled = _configService.Current.IsNameplateColorsEnabled; + var nameColors = _configService.Current.NameplateColors; + var isFriendOverride = _configService.Current.overrideFriendColor; + var isPartyOverride = _configService.Current.overridePartyColor; + + if (ImGui.Checkbox("Override name color of visible paired players", ref nameColorsEnabled)) + { + _configService.Current.IsNameplateColorsEnabled = nameColorsEnabled; + _configService.Save(); + _nameplateService.RequestRedraw(); + } + + _uiShared.DrawHelpText("This will override the nameplate colors for visible paired players in-game."); + + using (ImRaii.Disabled(!nameColorsEnabled)) + { + using var indent = ImRaii.PushIndent(); + if (InputDtrColors("Name color", ref nameColors)) + { + _configService.Current.NameplateColors = nameColors; + _configService.Save(); + _nameplateService.RequestRedraw(); + } + + if (ImGui.Checkbox("Override friend color", ref isFriendOverride)) + { + _configService.Current.overrideFriendColor = isFriendOverride; + _configService.Save(); + _nameplateService.RequestRedraw(); + } + + if (ImGui.Checkbox("Override party color", ref isPartyOverride)) + { + _configService.Current.overridePartyColor = isPartyOverride; + _configService.Save(); + _nameplateService.RequestRedraw(); + } + } + + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGui.TreePop(); + nameplateTree.MarkContentEnd(); } - - _uiShared.DrawHelpText("This will override the nameplate colors for visible paired players in-game."); - - using (ImRaii.Disabled(!nameColorsEnabled)) - { - using var indent = ImRaii.PushIndent(); - if (InputDtrColors("Name color", ref nameColors)) - { - _configService.Current.NameplateColors = nameColors; - _configService.Save(); - _nameplateService.RequestRedraw(); - } - - if (ImGui.Checkbox("Override friend color", ref isFriendOverride)) - { - _configService.Current.overrideFriendColor = isFriendOverride; - _configService.Save(); - _nameplateService.RequestRedraw(); - } - - if (ImGui.Checkbox("Override party color", ref isPartyOverride)) - { - _configService.Current.overridePartyColor = isPartyOverride; - _configService.Save(); - _nameplateService.RequestRedraw(); - } - } - - UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); - ImGui.TreePop(); - nameplateTree.MarkContentEnd(); } + + ImGui.Separator(); + + ImGui.EndChild(); + ImGui.EndGroup(); + + generalSelune.DrawHighlightOnly(ImGui.GetIO().DeltaTime); } - - ImGui.Separator(); - - ImGui.EndChild(); - ImGui.EndGroup(); - - generalSelune.DrawHighlightOnly(ImGui.GetIO().DeltaTime); } private void DrawGeneralNavigation() @@ -3916,6 +4012,39 @@ public class SettingsUi : WindowMediatorSubscriberBase } } + private static string GetLightfinderPresetGlyph(int index) + { + return LightFinderPlateHandler.NormalizeIconGlyph( + SeIconCharExtensions.ToIconString(LightfinderIconPresets[index].Icon)); + } + + private void RefreshLightfinderIconState() + { + var normalized = LightFinderPlateHandler.NormalizeIconGlyph(_configService.Current.LightfinderLabelIconGlyph); + _lightfinderIconInput = LightFinderPlateHandler.ToIconEditorString(normalized); + _lightfinderIconInputInitialized = true; + + _lightfinderIconPresetIndex = -1; + for (int i = 0; i < LightfinderIconPresets.Length; i++) + { + if (string.Equals(GetLightfinderPresetGlyph(i), normalized, StringComparison.Ordinal)) + { + _lightfinderIconPresetIndex = i; + break; + } + } + } + + private void ApplyLightfinderIcon(string normalizedGlyph, int presetIndex) + { + _configService.Current.LightfinderLabelIconGlyph = normalizedGlyph; + _configService.Save(); + _lightfinderIconInput = LightFinderPlateHandler.ToIconEditorString(normalizedGlyph); + _lightfinderIconPresetIndex = presetIndex; + _lightfinderIconInputInitialized = true; + } + + private void DrawServerServiceConfiguration(ServerStorage selectedServer, ref bool useOauth) { var serverName = selectedServer.ServerName;