diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 08065d1..b61ad55 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -294,7 +294,10 @@ public sealed class Plugin : IDalamudPlugin sp.GetRequiredService())); collection.AddSingleton(); collection.AddSingleton(s => new LightFinderScannerService(s.GetRequiredService>(), framework, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); - + collection.AddSingleton((s) => new LightFinderPlateHandler(s.GetRequiredService>(), + s.GetRequiredService(), pluginInterface, + s.GetRequiredService(), + objectTable, gameGui)); // add scoped services collection.AddScoped(); @@ -346,9 +349,7 @@ public sealed class Plugin : IDalamudPlugin pluginInterface, textureProvider, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); collection.AddScoped((s) => new NameplateService(s.GetRequiredService>(), s.GetRequiredService(), clientState, gameGui, objectTable, gameInteropProvider, - s.GetRequiredService(),s.GetRequiredService())); - collection.AddScoped((s) => new NameplateHandler(s.GetRequiredService>(), addonLifecycle, gameGui, - s.GetRequiredService(), s.GetRequiredService(), objectTable, s.GetRequiredService())); + s.GetRequiredService(), s.GetRequiredService())); collection.AddHostedService(p => p.GetRequiredService()); collection.AddHostedService(p => p.GetRequiredService()); @@ -365,6 +366,7 @@ public sealed class Plugin : IDalamudPlugin collection.AddHostedService(p => p.GetRequiredService()); collection.AddHostedService(p => p.GetRequiredService()); collection.AddHostedService(p => p.GetRequiredService()); + collection.AddHostedService(p => p.GetRequiredService()); }) .Build(); diff --git a/LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs b/LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs new file mode 100644 index 0000000..8002beb --- /dev/null +++ b/LightlessSync/Services/LightFinder/LightFinderPlateHandler.cs @@ -0,0 +1,249 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Interface; +using Dalamud.Plugin; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using LightlessSync.LightlessConfiguration; +using LightlessSync.Services.Mediator; +using LightlessSync.UI; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Collections.Immutable; +using System.Numerics; + +namespace LightlessSync.Services.LightFinder +{ + public class LightFinderPlateHandler : IHostedService, IMediatorSubscriber + { + private readonly ILogger _logger; + private readonly LightlessConfigService _configService; + private readonly IDalamudPluginInterface _pluginInterface; + private readonly IObjectTable _gameObjects; + private readonly IGameGui _gameGui; + + private const float _defaultNameplateDistance = 15.0f; + private ImmutableHashSet _activeBroadcastingCids = []; + private readonly Dictionary _smoothed = []; + private readonly float _defaultHeightOffset = 0f; + + public LightlessMediator Mediator { get; } + + public LightFinderPlateHandler( + ILogger logger, + LightlessMediator mediator, + IDalamudPluginInterface dalamudPluginInterface, + LightlessConfigService configService, + IObjectTable gameObjects, + IGameGui gameGui) + { + _logger = logger; + Mediator = mediator; + _pluginInterface = dalamudPluginInterface; + _configService = configService; + _gameObjects = gameObjects; + _gameGui = gameGui; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Starting LightFinderPlateHandler..."); + + _pluginInterface.UiBuilder.Draw += OnDraw; + + _logger.LogInformation("LightFinderPlateHandler started."); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Stopping LightFinderPlateHandler..."); + + _pluginInterface.UiBuilder.Draw -= OnDraw; + + _logger.LogInformation("LightFinderPlateHandler stopped."); + return Task.CompletedTask; + } + + private unsafe void OnDraw() + { + if (!_configService.Current.BroadcastEnabled) + return; + + if (_activeBroadcastingCids.Count == 0) + return; + + var drawList = ImGui.GetForegroundDrawList(); + + foreach (var obj in _gameObjects.PlayerObjects.OfType()) + { + //Double check to be sure, should always be true due to OfType filter above + if (obj is not IPlayerCharacter player) + continue; + + if (player.Address == IntPtr.Zero) + continue; + + var hashedCID = DalamudUtilService.GetHashedCIDFromPlayerPointer(player.Address); + if (!_activeBroadcastingCids.Contains(hashedCID)) + continue; + + //Approximate check if nameplate should be visible (at short distances) + if (!ShouldApproximateNameplateVisible(player)) + continue; + + if (!TryGetApproxNameplateScreenPos(player, out var rawScreenPos)) + continue; + + var rawVector3 = new Vector3(rawScreenPos.X, rawScreenPos.Y, 0f); + + if (rawVector3 == Vector3.Zero) + { + _smoothed.Remove(obj); + continue; + } + + //Possible have to rework this. Currently just a simple distance check to avoid jitter. + Vector3 smoothedVector3; + + if (_smoothed.TryGetValue(obj, out var lastVector3)) + { + var deltaVector2 = new Vector2(rawVector3.X - lastVector3.X, rawVector3.Y - lastVector3.Y); + if (deltaVector2.Length() < 1f) + smoothedVector3 = lastVector3; + else + smoothedVector3 = rawVector3; + } + else + { + smoothedVector3 = rawVector3; + } + + _smoothed[obj] = smoothedVector3; + + var screenPos = new Vector2(smoothedVector3.X, smoothedVector3.Y); + + var radiusWorld = Math.Max(player.HitboxRadius, 0.5f); + var radiusPx = radiusWorld * 8.0f; + var offsetPx = GetScreenOffset(player); + var drawPos = new Vector2(screenPos.X, screenPos.Y - offsetPx); + + var fillColor = ImGui.GetColorU32(UiSharedService.Color(UIColors.Get("Lightfinder"))); + var outlineColor = ImGui.GetColorU32(UiSharedService.Color(UIColors.Get("LightfinderEdge"))); + + drawList.AddCircleFilled(drawPos, radiusPx, fillColor); + drawList.AddCircle(drawPos, radiusPx, outlineColor, 0, 2.0f); + + var label = "LightFinder"; + var icon = FontAwesomeIcon.Bullseye.ToIconString(); + + ImGui.PushFont(UiBuilder.IconFont); + var iconSize = ImGui.CalcTextSize(icon); + var iconPos = new Vector2(drawPos.X - iconSize.X / 2f, drawPos.Y - radiusPx - iconSize.Y - 2f); + drawList.AddText(iconPos, fillColor, icon); + ImGui.PopFont(); + + /* var scale = 1.4f; + var font = ImGui.GetFont(); + var baseFontSize = ImGui.GetFontSize(); + var fontSize = baseFontSize * scale; + + var baseTextSize = ImGui.CalcTextSize(label); + var textSize = baseTextSize * scale; + + var textPos = new Vector2( + drawPos.X - textSize.X / 2f, + drawPos.Y - radiusPx - textSize.Y - 2f + ); + + drawList.AddText(font, fontSize, textPos, fillColor, label); */ + } + } + + // Get screen offset based on distance to local player (to scale size appropriately) + // I need to fine tune these values still + private float GetScreenOffset(IPlayerCharacter player) + { + var local = _gameObjects.LocalPlayer; + if (local == null) + return 32.1f; + + var delta = player.Position - local.Position; + var dist = MathF.Sqrt(delta.X * delta.X + delta.Z * delta.Z); + + const float minDist = 2.1f; + const float maxDist = 30.4f; + dist = Math.Clamp(dist, minDist, maxDist); + + var t = 1f - (dist - minDist) / (maxDist - minDist); + + const float minOffset = 24.4f; + const float maxOffset = 56.4f; + return minOffset + (maxOffset - minOffset) * t; + } + + private bool TryGetApproxNameplateScreenPos(IPlayerCharacter player, out Vector2 screenPos) + { + screenPos = default; + + var worldPos = player.Position; + + var visualHeight = GetVisualHeight(player); + + worldPos.Y += (visualHeight + 1.2f) + _defaultHeightOffset; + + if (!_gameGui.WorldToScreen(worldPos, out var raw)) + return false; + + screenPos = raw; + return true; + } + + // Approximate check to see if nameplate would be visible based on distance and screen position + // Also has to be fine tuned still + private bool ShouldApproximateNameplateVisible(IPlayerCharacter player) + { + var local = _gameObjects.LocalPlayer; + if (local == null) + return false; + + var delta = player.Position - local.Position; + var distance2D = MathF.Sqrt(delta.X * delta.X + delta.Z * delta.Z); + if (distance2D > _defaultNameplateDistance) + return false; + + var verticalDelta = MathF.Abs(delta.Y); + if (verticalDelta > 3.4f) + return false; + + return TryGetApproxNameplateScreenPos(player, out _); + } + + private static unsafe float GetVisualHeight(IPlayerCharacter player) + { + var gameObject = (GameObject*)player.Address; + if (gameObject == null) + return Math.Max(player.HitboxRadius * 2.0f, 1.7f); // fallback + + // This should account for transformations (sitting, crouching, etc.) + var radius = gameObject->GetRadius(adjustByTransformation: true); + if (radius <= 0) + radius = Math.Max(player.HitboxRadius * 2.0f, 1.7f); + + return radius; + } + + // Update the set of active broadcasting CIDs (Same uses as in NameplateHnadler before) + public void UpdateBroadcastingCids(IEnumerable cids) + { + var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal); + if (ReferenceEquals(_activeBroadcastingCids, newSet) || _activeBroadcastingCids.SetEquals(newSet)) + return; + + _activeBroadcastingCids = newSet; + if (_logger.IsEnabled(LogLevel.Information)) + _logger.LogInformation("Active broadcast CIDs: {Cids}", string.Join(',', _activeBroadcastingCids)); + } + } +} \ No newline at end of file diff --git a/LightlessSync/Services/LightFinder/LightFinderScannerService.cs b/LightlessSync/Services/LightFinder/LightFinderScannerService.cs index a0ea3e6..52ff1dc 100644 --- a/LightlessSync/Services/LightFinder/LightFinderScannerService.cs +++ b/LightlessSync/Services/LightFinder/LightFinderScannerService.cs @@ -14,7 +14,7 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase private readonly IFramework _framework; private readonly LightFinderService _broadcastService; - private readonly NameplateHandler _nameplateHandler; + private readonly LightFinderPlateHandler _lightFinderPlateHandler; private readonly ConcurrentDictionary _broadcastCache = new(StringComparer.Ordinal); private readonly Queue _lookupQueue = new(); @@ -41,22 +41,21 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase IFramework framework, LightFinderService broadcastService, LightlessMediator mediator, - NameplateHandler nameplateHandler, + LightFinderPlateHandler lightFinderPlateHandler, ActorObjectService actorTracker) : base(logger, mediator) { _logger = logger; _actorTracker = actorTracker; _broadcastService = broadcastService; - _nameplateHandler = nameplateHandler; + _lightFinderPlateHandler = lightFinderPlateHandler; _logger = logger; _framework = framework; _framework.Update += OnFrameworkUpdate; Mediator.Subscribe(this, OnBroadcastStatusChanged); - _cleanupTask = Task.Run(ExpiredBroadcastCleanupLoop); + _cleanupTask = Task.Run(ExpiredBroadcastCleanupLoop, _cleanupCts.Token); - _nameplateHandler.Init(); _actorTracker = actorTracker; } @@ -129,7 +128,7 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase .Select(e => e.Key) .ToList(); - _nameplateHandler.UpdateBroadcastingCids(activeCids); + _lightFinderPlateHandler.UpdateBroadcastingCids(activeCids); UpdateSyncshellBroadcasts(); } @@ -142,7 +141,7 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase _lookupQueuedCids.Clear(); _syncshellCids.Clear(); - _nameplateHandler.UpdateBroadcastingCids(Enumerable.Empty()); + _lightFinderPlateHandler.UpdateBroadcastingCids([]); } } @@ -243,6 +242,5 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase _cleanupTask?.Wait(100); _cleanupCts.Dispose(); - _nameplateHandler.Uninit(); } } diff --git a/LightlessSync/Services/NameplateHandler.cs b/LightlessSync/Services/NameplateHandler.cs deleted file mode 100644 index 808242d..0000000 --- a/LightlessSync/Services/NameplateHandler.cs +++ /dev/null @@ -1,693 +0,0 @@ -using Dalamud.Game.Addon.Lifecycle; -using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; -using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Game.Text; -using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.System.Framework; -using FFXIVClientStructs.FFXIV.Client.UI; -using FFXIVClientStructs.FFXIV.Component.GUI; -using LightlessSync.LightlessConfiguration; -using LightlessSync.Services.Mediator; -using LightlessSync.UI; -using LightlessSync.UI.Services; -using LightlessSync.Utils; -using LightlessSync.UtilsEnum.Enum; - -// Created using https://github.com/PunishedPineapple/Distance as a reference, thank you! - -using Microsoft.Extensions.Logging; -using System.Collections.Immutable; -using System.Globalization; - -namespace LightlessSync.Services; - -public unsafe class NameplateHandler : IMediatorSubscriber -{ - private readonly ILogger _logger; - private readonly IAddonLifecycle _addonLifecycle; - private readonly IGameGui _gameGui; - private readonly IObjectTable _objectTable; - private readonly LightlessConfigService _configService; - private readonly PairUiService _pairUiService; - private readonly LightlessMediator _mediator; - public LightlessMediator Mediator => _mediator; - - private bool _mEnabled = false; - private bool _needsLabelRefresh = false; - private AddonNamePlate* _mpNameplateAddon = null; - private readonly AtkTextNode*[] _mTextNodes = new AtkTextNode*[AddonNamePlate.NumNamePlateObjects]; - private readonly int[] _cachedNameplateTextWidths = new int[AddonNamePlate.NumNamePlateObjects]; - private readonly int[] _cachedNameplateTextHeights = new int[AddonNamePlate.NumNamePlateObjects]; - private readonly int[] _cachedNameplateContainerHeights = new int[AddonNamePlate.NumNamePlateObjects]; - private readonly int[] _cachedNameplateTextOffsets = new int[AddonNamePlate.NumNamePlateObjects]; - - internal const uint mNameplateNodeIDBase = 0x7D99D500; - private const string DefaultLabelText = "LightFinder"; - private const SeIconChar DefaultIcon = SeIconChar.Hyadelyn; - private static readonly string DefaultIconGlyph = SeIconCharExtensions.ToIconString(DefaultIcon); - - private ImmutableHashSet _activeBroadcastingCids = []; - - public NameplateHandler(ILogger logger, IAddonLifecycle addonLifecycle, IGameGui gameGui, LightlessConfigService configService, LightlessMediator mediator, IObjectTable objectTable, PairUiService pairUiService) - { - _logger = logger; - _addonLifecycle = addonLifecycle; - _gameGui = gameGui; - _configService = configService; - _mediator = mediator; - _objectTable = objectTable; - _pairUiService = pairUiService; - - System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue); - } - - internal void Init() - { - EnableNameplate(); - _mediator.Subscribe(this, OnTick); - } - - internal void Uninit() - { - DisableNameplate(); - DestroyNameplateNodes(); - _mediator.Unsubscribe(this); - _mpNameplateAddon = null; - } - - internal void EnableNameplate() - { - if (!_mEnabled) - { - try - { - _addonLifecycle.RegisterListener(AddonEvent.PostDraw, "NamePlate", NameplateDrawDetour); - _mEnabled = true; - } - catch (Exception e) - { - _logger.LogError($"Unknown error while trying to enable nameplate distances:\n{e}"); - DisableNameplate(); - } - } - } - - internal void DisableNameplate() - { - if (_mEnabled) - { - try - { - _addonLifecycle.UnregisterListener(NameplateDrawDetour); - } - catch (Exception e) - { - _logger.LogError($"Unknown error while unregistering nameplate listener:\n{e}"); - } - - _mEnabled = false; - HideAllNameplateNodes(); - } - } - - private void NameplateDrawDetour(AddonEvent type, AddonArgs args) - { - if (args.Addon.Address == nint.Zero) - { - if (_logger.IsEnabled(LogLevel.Warning)) - _logger.LogWarning("Nameplate draw detour received a null addon address, skipping update."); - return; - } - - var pNameplateAddon = (AddonNamePlate*)args.Addon.Address; - - if (_mpNameplateAddon != pNameplateAddon) - { - for (int i = 0; i < _mTextNodes.Length; ++i) _mTextNodes[i] = null; - System.Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length); - System.Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length); - System.Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length); - System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue); - _mpNameplateAddon = pNameplateAddon; - if (_mpNameplateAddon != null) CreateNameplateNodes(); - } - - UpdateNameplateNodes(); - } - - private void CreateNameplateNodes() - { - for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i) - { - var nameplateObject = GetNameplateObject(i); - if (nameplateObject == null) - continue; - - var rootNode = nameplateObject.Value.RootComponentNode; - if (rootNode == null || rootNode->Component == null) - continue; - - var pNameplateResNode = nameplateObject.Value.NameContainer; - if (pNameplateResNode == null) - continue; - if (pNameplateResNode->ChildNode == null) - continue; - - var pNewNode = AtkNodeHelpers.CreateOrphanTextNode(mNameplateNodeIDBase + (uint)i, TextFlags.Edge | TextFlags.Glare); - - if (pNewNode != null) - { - var pLastChild = pNameplateResNode->ChildNode; - while (pLastChild->PrevSiblingNode != null) pLastChild = pLastChild->PrevSiblingNode; - pNewNode->AtkResNode.NextSiblingNode = pLastChild; - pNewNode->AtkResNode.ParentNode = pNameplateResNode; - pLastChild->PrevSiblingNode = (AtkResNode*)pNewNode; - rootNode->Component->UldManager.UpdateDrawNodeList(); - pNewNode->AtkResNode.SetUseDepthBasedPriority(true); - _mTextNodes[i] = pNewNode; - } - } - } - - private void DestroyNameplateNodes() - { - var currentHandle = _gameGui.GetAddonByName("NamePlate", 1); - if (currentHandle.Address == nint.Zero) - { - if (_logger.IsEnabled(LogLevel.Warning)) - _logger.LogWarning("Unable to destroy nameplate nodes because the NamePlate addon is not available."); - return; - } - - var pCurrentNameplateAddon = (AddonNamePlate*)currentHandle.Address; - if (_mpNameplateAddon == null) - return; - - if (_mpNameplateAddon != pCurrentNameplateAddon) - { - if (_logger.IsEnabled(LogLevel.Warning)) - _logger.LogWarning("Skipping nameplate node destroy due to addon address mismatch (cached {Cached}, current {Current}).", (IntPtr)_mpNameplateAddon, (IntPtr)pCurrentNameplateAddon); - return; - } - - for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i) - { - var pTextNode = _mTextNodes[i]; - var pNameplateNode = GetNameplateComponentNode(i); - if (pTextNode != null && (pNameplateNode == null || pNameplateNode->Component == null)) - { - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("Skipping destroy for nameplate {Index} because its component node is unavailable.", i); - continue; - } - - if (pTextNode != null && pNameplateNode != null && pNameplateNode->Component != null) - { - try - { - if (pTextNode->AtkResNode.PrevSiblingNode != null) - pTextNode->AtkResNode.PrevSiblingNode->NextSiblingNode = pTextNode->AtkResNode.NextSiblingNode; - if (pTextNode->AtkResNode.NextSiblingNode != null) - pTextNode->AtkResNode.NextSiblingNode->PrevSiblingNode = pTextNode->AtkResNode.PrevSiblingNode; - pNameplateNode->Component->UldManager.UpdateDrawNodeList(); - pTextNode->AtkResNode.Destroy(free: true); - _mTextNodes[i] = null; - } - catch (Exception e) - { - if (_logger.IsEnabled(LogLevel.Error)) - _logger.LogError("Unknown error while removing text node 0x{textNode} for nameplate {i} on component node 0x{nameplateNode}:\n{e}", (IntPtr)pTextNode, i, (IntPtr)pNameplateNode, e); - } - } - } - - System.Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length); - System.Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length); - System.Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length); - System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue); - } - - private void HideAllNameplateNodes() - { - for (int i = 0; i < _mTextNodes.Length; ++i) - { - HideNameplateTextNode(i); - } - } - - private void UpdateNameplateNodes() - { - var currentHandle = _gameGui.GetAddonByName("NamePlate"); - if (currentHandle.Address == nint.Zero) - { - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("NamePlate addon unavailable during update, skipping label refresh."); - return; - } - - var currentAddon = (AddonNamePlate*)currentHandle.Address; - if (_mpNameplateAddon == null || currentAddon == null || currentAddon != _mpNameplateAddon) - { - if (_mpNameplateAddon != null && _logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("Cached NamePlate addon pointer differs from current: waiting for new hook (cached {Cached}, current {Current}).", (IntPtr)_mpNameplateAddon, (IntPtr)currentAddon); - return; - } - - var framework = Framework.Instance(); - if (framework == null) - { - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("Framework instance unavailable during nameplate update, skipping."); - return; - } - - var uiModule = framework->GetUIModule(); - if (uiModule == null) - { - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("UI module unavailable during nameplate update, skipping."); - return; - } - - var ui3DModule = uiModule->GetUI3DModule(); - if (ui3DModule == null) - { - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("UI3D module unavailable during nameplate update, skipping."); - return; - } - - var vec = ui3DModule->NamePlateObjectInfoPointers; - if (vec.IsEmpty) - return; - - var visibleUserIdsSnapshot = VisibleUserIds; - - var safeCount = System.Math.Min( - ui3DModule->NamePlateObjectInfoCount, - vec.Length - ); - - for (int i = 0; i < safeCount; ++i) - { - var config = _configService.Current; - - var objectInfoPtr = vec[i]; - if (objectInfoPtr == null) - continue; - - var objectInfo = objectInfoPtr.Value; - if (objectInfo == null || objectInfo->GameObject == null) - continue; - - var nameplateIndex = objectInfo->NamePlateIndex; - if (nameplateIndex < 0 || nameplateIndex >= AddonNamePlate.NumNamePlateObjects) - continue; - - var pNode = _mTextNodes[nameplateIndex]; - if (pNode == null) - continue; - - var gameObject = objectInfo->GameObject; - if ((ObjectKind)gameObject->ObjectKind != ObjectKind.Player) - { - pNode->AtkResNode.ToggleVisibility(enable: false); - continue; - } - - // CID gating - var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)gameObject); - if (cid == null || !_activeBroadcastingCids.Contains(cid)) - { - pNode->AtkResNode.ToggleVisibility(enable: false); - continue; - } - - var local = _objectTable.LocalPlayer; - if (!config.LightfinderLabelShowOwn && local != null && - objectInfo->GameObject->GetGameObjectId() == local.GameObjectId) - { - pNode->AtkResNode.ToggleVisibility(enable: false); - continue; - } - - var hidePaired = !config.LightfinderLabelShowPaired; - - var goId = (ulong)gameObject->GetGameObjectId(); - if (hidePaired && visibleUserIdsSnapshot.Contains(goId)) - { - pNode->AtkResNode.ToggleVisibility(enable: false); - continue; - } - - var nameplateObject = _mpNameplateAddon->NamePlateObjectArray[nameplateIndex]; - 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) - { - _logger.LogDebug("Nameplate {Index} missing required nodes during update, skipping.", nameplateIndex); - pNode->AtkResNode.ToggleVisibility(enable: false); - continue; - } - - root->Component->UldManager.UpdateDrawNodeList(); - - bool isVisible = - ((marker != null) && marker->AtkResNode.IsVisible()) || - (nameContainer->IsVisible() && nameText->AtkResNode.IsVisible()) || - config.LightfinderLabelShowHidden; - - pNode->AtkResNode.ToggleVisibility(isVisible); - if (!isVisible) - continue; - - var labelColor = UIColors.Get("Lightfinder"); - var edgeColor = UIColors.Get("LightfinderEdge"); - - var scaleMultiplier = System.Math.Clamp(config.LightfinderLabelScale, 0.5f, 2.0f); - var baseScale = config.LightfinderLabelUseIcon ? 1.0f : 0.5f; - var effectiveScale = baseScale * scaleMultiplier; - var labelContent = config.LightfinderLabelUseIcon - ? NormalizeIconGlyph(config.LightfinderLabelIconGlyph) - : DefaultLabelText; - - pNode->FontType = config.LightfinderLabelUseIcon ? FontType.Axis : FontType.MiedingerMed; - pNode->AtkResNode.SetScale(effectiveScale, effectiveScale); - var nodeWidth = (int)pNode->AtkResNode.GetWidth(); - if (nodeWidth <= 0) - nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale); - var nodeHeight = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale); - var baseFontSize = config.LightfinderLabelUseIcon ? 36f : 24f; - var computedFontSize = (int)System.Math.Round(baseFontSize * scaleMultiplier); - pNode->FontSize = (byte)System.Math.Clamp(computedFontSize, 1, 255); - AlignmentType alignment; - - var textScaleY = nameText->AtkResNode.ScaleY; - if (textScaleY <= 0f) - textScaleY = 1f; - - var blockHeight = System.Math.Abs((int)nameplateObject.TextH); - if (blockHeight > 0) - { - _cachedNameplateTextHeights[nameplateIndex] = blockHeight; - } - else - { - blockHeight = _cachedNameplateTextHeights[nameplateIndex]; - } - - if (blockHeight <= 0) - { - blockHeight = GetScaledTextHeight(nameText); - if (blockHeight <= 0) - blockHeight = nodeHeight; - - _cachedNameplateTextHeights[nameplateIndex] = blockHeight; - } - - var containerHeight = (int)nameContainer->Height; - if (containerHeight > 0) - { - _cachedNameplateContainerHeights[nameplateIndex] = containerHeight; - } - else - { - containerHeight = _cachedNameplateContainerHeights[nameplateIndex]; - } - - if (containerHeight <= 0) - { - containerHeight = blockHeight + (int)System.Math.Round(8 * textScaleY); - if (containerHeight <= blockHeight) - containerHeight = blockHeight + 1; - - _cachedNameplateContainerHeights[nameplateIndex] = containerHeight; - } - - var blockTop = containerHeight - blockHeight; - if (blockTop < 0) - blockTop = 0; - var verticalPadding = (int)System.Math.Round(4 * effectiveScale); - - var positionY = blockTop - verticalPadding - nodeHeight; - - var textWidth = System.Math.Abs((int)nameplateObject.TextW); - if (textWidth <= 0) - { - textWidth = GetScaledTextWidth(nameText); - if (textWidth <= 0) - textWidth = nodeWidth; - } - - if (textWidth > 0) - { - _cachedNameplateTextWidths[nameplateIndex] = textWidth; - } - - var textOffset = (int)System.Math.Round(nameText->AtkResNode.X); - var hasValidOffset = true; - - if (System.Math.Abs((int)nameplateObject.TextW) > 0 || textOffset != 0) - { - _cachedNameplateTextOffsets[nameplateIndex] = textOffset; - } - else - { - hasValidOffset = false; - } - int positionX; - - - if (!config.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal))) - labelContent = DefaultLabelText; - - pNode->FontType = config.LightfinderLabelUseIcon ? FontType.Axis : FontType.MiedingerMed; - - pNode->SetText(labelContent); - - if (!config.LightfinderLabelUseIcon) - { - pNode->TextFlags &= ~TextFlags.AutoAdjustNodeSize; - pNode->AtkResNode.Width = 0; - nodeWidth = (int)pNode->AtkResNode.GetWidth(); - if (nodeWidth <= 0) - nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale); - pNode->AtkResNode.Width = (ushort)nodeWidth; - } - else - { - pNode->TextFlags |= TextFlags.AutoAdjustNodeSize; - pNode->AtkResNode.Width = 0; - nodeWidth = pNode->AtkResNode.GetWidth(); - } - - - if (config.LightfinderAutoAlign && nameContainer != null && hasValidOffset) - { - var nameplateWidth = (int)nameContainer->Width; - - int leftPos = nameplateWidth / 8; - int rightPos = nameplateWidth - nodeWidth - (nameplateWidth / 8); - int centrePos = (nameplateWidth - nodeWidth) / 2; - int staticMargin = 24; - int calcMargin = (int)(nameplateWidth * 0.08f); - - switch (config.LabelAlignment) - { - case LabelAlignment.Left: - positionX = config.LightfinderLabelUseIcon ? leftPos + staticMargin : leftPos; - alignment = AlignmentType.BottomLeft; - break; - case LabelAlignment.Right: - positionX = config.LightfinderLabelUseIcon ? rightPos - staticMargin : nameplateWidth - nodeWidth + calcMargin; - alignment = AlignmentType.BottomRight; - break; - default: - positionX = config.LightfinderLabelUseIcon ? centrePos : centrePos + calcMargin; - alignment = AlignmentType.Bottom; - break; - } - } - else - { - positionX = 58 + config.LightfinderLabelOffsetX; - alignment = AlignmentType.Bottom; - } - - positionY += config.LightfinderLabelOffsetY; - - alignment = (AlignmentType)System.Math.Clamp((int)alignment, 0, 8); - pNode->AtkResNode.SetUseDepthBasedPriority(enable: true); - - pNode->AtkResNode.Color.A = 255; - - pNode->TextColor.R = (byte)(labelColor.X * 255); - pNode->TextColor.G = (byte)(labelColor.Y * 255); - pNode->TextColor.B = (byte)(labelColor.Z * 255); - pNode->TextColor.A = (byte)(labelColor.W * 255); - - pNode->EdgeColor.R = (byte)(edgeColor.X * 255); - pNode->EdgeColor.G = (byte)(edgeColor.Y * 255); - pNode->EdgeColor.B = (byte)(edgeColor.Z * 255); - pNode->EdgeColor.A = (byte)(edgeColor.W * 255); - - - if (!config.LightfinderLabelUseIcon) - { - pNode->AlignmentType = AlignmentType.Bottom; - } - else - { - pNode->AlignmentType = alignment; - } - pNode->AtkResNode.SetPositionShort( - (short)System.Math.Clamp(positionX, short.MinValue, short.MaxValue), - (short)System.Math.Clamp(positionY, short.MinValue, short.MaxValue) - ); - var computedLineSpacing = (int)System.Math.Round(24 * scaleMultiplier); - pNode->LineSpacing = (byte)System.Math.Clamp(computedLineSpacing, 0, byte.MaxValue); - pNode->CharSpacing = 1; - pNode->TextFlags = config.LightfinderLabelUseIcon - ? TextFlags.Edge | TextFlags.Glare | TextFlags.AutoAdjustNodeSize - : TextFlags.Edge | TextFlags.Glare; - } - } - - private static unsafe int GetScaledTextHeight(AtkTextNode* node) - { - if (node == null) - return 0; - - var resNode = &node->AtkResNode; - var rawHeight = (int)resNode->GetHeight(); - if (rawHeight <= 0 && node->LineSpacing > 0) - rawHeight = node->LineSpacing; - if (rawHeight <= 0) - rawHeight = AtkNodeHelpers.DefaultTextNodeHeight; - - var scale = resNode->ScaleY; - if (scale <= 0f) - scale = 1f; - - var computed = (int)System.Math.Round(rawHeight * scale); - return System.Math.Max(1, computed); - } - - private static unsafe int GetScaledTextWidth(AtkTextNode* node) - { - if (node == null) - return 0; - - var resNode = &node->AtkResNode; - var rawWidth = (int)resNode->GetWidth(); - if (rawWidth <= 0) - rawWidth = AtkNodeHelpers.DefaultTextNodeWidth; - - var scale = resNode->ScaleX; - if (scale <= 0f) - scale = 1f; - - var computed = (int)System.Math.Round(rawWidth * scale); - return System.Math.Max(1, computed); - } - - internal static string NormalizeIconGlyph(string? rawInput) - { - if (string.IsNullOrWhiteSpace(rawInput)) - return DefaultIconGlyph; - - var trimmed = rawInput.Trim(); - - if (Enum.TryParse(trimmed, true, out var iconEnum)) - return SeIconCharExtensions.ToIconString(iconEnum); - - var hexCandidate = trimmed.StartsWith("0x", StringComparison.OrdinalIgnoreCase) - ? trimmed[2..] - : trimmed; - - if (ushort.TryParse(hexCandidate, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var hexValue)) - return char.ConvertFromUtf32(hexValue); - - var enumerator = trimmed.EnumerateRunes(); - if (enumerator.MoveNext()) - return enumerator.Current.ToString(); - - return DefaultIconGlyph; - } - - internal static string ToIconEditorString(string? rawInput) - { - var normalized = NormalizeIconGlyph(rawInput); - var runeEnumerator = normalized.EnumerateRunes(); - return runeEnumerator.MoveNext() - ? runeEnumerator.Current.Value.ToString("X4", CultureInfo.InvariantCulture) - : DefaultIconGlyph; - } - private void HideNameplateTextNode(int i) - { - var pNode = _mTextNodes[i]; - if (pNode != null) - { - pNode->AtkResNode.ToggleVisibility(false); - } - } - - private AddonNamePlate.NamePlateObject? GetNameplateObject(int i) - { - if (i < AddonNamePlate.NumNamePlateObjects && - _mpNameplateAddon != null && - _mpNameplateAddon->NamePlateObjectArray[i].RootComponentNode != null) - { - return _mpNameplateAddon->NamePlateObjectArray[i]; - } - return null; - } - - private AtkComponentNode* GetNameplateComponentNode(int i) - { - var nameplateObject = GetNameplateObject(i); - return nameplateObject != null ? nameplateObject.Value.RootComponentNode : null; - } - - private HashSet VisibleUserIds - => [.. _pairUiService.GetSnapshot().PairsByUid.Values - .Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue) - .Select(u => (ulong)u.PlayerCharacterId)]; - - public void FlagRefresh() - { - _needsLabelRefresh = true; - } - - public void OnTick(PriorityFrameworkUpdateMessage _) - { - if (_needsLabelRefresh) - { - UpdateNameplateNodes(); - _needsLabelRefresh = false; - } - } - - public void UpdateBroadcastingCids(IEnumerable cids) - { - var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal); - if (ReferenceEquals(_activeBroadcastingCids, newSet) || _activeBroadcastingCids.SetEquals(newSet)) - return; - - _activeBroadcastingCids = newSet; - if (_logger.IsEnabled(LogLevel.Information)) - _logger.LogInformation("Active broadcast CIDs: {Cids}", string.Join(',', _activeBroadcastingCids)); - FlagRefresh(); - } - - public void ClearNameplateCaches() - { - System.Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length); - System.Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length); - System.Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length); - System.Array.Fill(_cachedNameplateTextOffsets, int.MinValue); - } -} diff --git a/LightlessSync/UI/EditProfileUi.Group.cs b/LightlessSync/UI/EditProfileUi.Group.cs index cae1eeb..6e8f6d3 100644 --- a/LightlessSync/UI/EditProfileUi.Group.cs +++ b/LightlessSync/UI/EditProfileUi.Group.cs @@ -277,7 +277,7 @@ public partial class EditProfileUi if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile picture")) { - _fileDialogManager.OpenFileDialog("Select syncshell profile picture", ImageFileDialogFilter, (success, file) => + _fileDialogManager.OpenFileDialog("Select syncshell profile picture", _imageFileDialogFilter, (success, file) => { if (!success || string.IsNullOrEmpty(file)) return; @@ -305,7 +305,7 @@ public partial class EditProfileUi if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile banner")) { - _fileDialogManager.OpenFileDialog("Select syncshell profile banner", ImageFileDialogFilter, (success, file) => + _fileDialogManager.OpenFileDialog("Select syncshell profile banner", _imageFileDialogFilter, (success, file) => { if (!success || string.IsNullOrEmpty(file)) return; diff --git a/LightlessSync/UI/EditProfileUi.cs b/LightlessSync/UI/EditProfileUi.cs index 3c9b8ae..78c38ed 100644 --- a/LightlessSync/UI/EditProfileUi.cs +++ b/LightlessSync/UI/EditProfileUi.cs @@ -19,12 +19,7 @@ using Microsoft.Extensions.Logging; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.PixelFormats; -using System; -using System.Collections.Generic; -using System.IO; using System.Numerics; -using System.Threading.Tasks; -using System.Linq; using LightlessSync.Services.Profiles; namespace LightlessSync.UI; @@ -56,9 +51,9 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase "webp", "bmp" }; - private const string ImageFileDialogFilter = "Images{.png,.jpg,.jpeg,.webp,.bmp}"; + private const string _imageFileDialogFilter = "Images{.png,.jpg,.jpeg,.webp,.bmp}"; private readonly List _tagEditorSelection = new(); - private int[] _profileTagIds = Array.Empty(); + private int[] _profileTagIds = []; private readonly List _tagPreviewSegments = new(); private enum ProfileEditorMode { @@ -77,8 +72,8 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase private byte[]? _queuedBannerImage; private readonly Vector4 _tagBackgroundColor = new(0.18f, 0.18f, 0.18f, 0.95f); private readonly Vector4 _tagBorderColor = new(0.35f, 0.35f, 0.35f, 0.4f); - private const int MaxProfileTags = 12; - private const int AvailableTagsPerPage = 6; + private const int _maxProfileTags = 12; + private const int _availableTagsPerPage = 6; private int _availableTagPage; private UserData? _selfProfileUserData; private string _descriptionText = string.Empty; @@ -92,10 +87,10 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase private bool _wasOpen; private Vector4 _currentBg = new(0.15f, 0.15f, 0.15f, 1f); - private bool textEnabled; - private bool glowEnabled; - private Vector4 textColor; - private Vector4 glowColor; + private bool _textEnabled; + private bool _glowEnabled; + private Vector4 _textColor; + private Vector4 _glowColor; private sealed record VanityState(bool TextEnabled, bool GlowEnabled, Vector4 TextColor, Vector4 GlowColor); private VanityState? _savedVanity; @@ -154,13 +149,13 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase private void LoadVanity() { - textEnabled = !string.IsNullOrEmpty(_apiController.TextColorHex); - glowEnabled = !string.IsNullOrEmpty(_apiController.TextGlowColorHex); + _textEnabled = !string.IsNullOrEmpty(_apiController.TextColorHex); + _glowEnabled = !string.IsNullOrEmpty(_apiController.TextGlowColorHex); - textColor = textEnabled ? UIColors.HexToRgba(_apiController.TextColorHex!) : Vector4.One; - glowColor = glowEnabled ? UIColors.HexToRgba(_apiController.TextGlowColorHex!) : Vector4.Zero; + _textColor = _textEnabled ? UIColors.HexToRgba(_apiController.TextColorHex!) : Vector4.One; + _glowColor = _glowEnabled ? UIColors.HexToRgba(_apiController.TextGlowColorHex!) : Vector4.Zero; - _savedVanity = new VanityState(textEnabled, glowEnabled, textColor, glowColor); + _savedVanity = new VanityState(_textEnabled, _glowEnabled, _textColor, _glowColor); } public override async void OnOpen() @@ -465,7 +460,7 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile picture")) { var existingBanner = GetCurrentProfileBannerBase64(profile); - _fileDialogManager.OpenFileDialog("Select new Profile picture", ImageFileDialogFilter, (success, file) => + _fileDialogManager.OpenFileDialog("Select new Profile picture", _imageFileDialogFilter, (success, file) => { if (!success) return; _ = Task.Run(async () => @@ -529,7 +524,7 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile banner")) { var existingProfile = GetCurrentProfilePictureBase64(profile); - _fileDialogManager.OpenFileDialog("Select new Profile banner", ImageFileDialogFilter, (success, file) => + _fileDialogManager.OpenFileDialog("Select new Profile banner", _imageFileDialogFilter, (success, file) => { if (!success) return; _ = Task.Run(async () => @@ -686,7 +681,7 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase var defaultTextColorU32 = ImGui.GetColorU32(ImGuiCol.Text); var selectedCount = _tagEditorSelection.Count; - ImGui.TextColored(UIColors.Get("LightlessBlue"), $"Selected Tags ({selectedCount}/{MaxProfileTags})"); + ImGui.TextColored(UIColors.Get("LightlessBlue"), $"Selected Tags ({selectedCount}/{_maxProfileTags})"); int? tagToRemove = null; int? moveUpRequest = null; @@ -766,9 +761,9 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase if (tagToRemove.HasValue) _tagEditorSelection.Remove(tagToRemove.Value); - bool limitReached = _tagEditorSelection.Count >= MaxProfileTags; + bool limitReached = _tagEditorSelection.Count >= _maxProfileTags; if (limitReached) - UiSharedService.ColorTextWrapped($"You have reached the maximum of {MaxProfileTags} tags. Remove one before adding more.", UIColors.Get("DimRed")); + UiSharedService.ColorTextWrapped($"You have reached the maximum of {_maxProfileTags} tags. Remove one before adding more.", UIColors.Get("DimRed")); ImGui.Dummy(new Vector2(0f, 6f * scale)); ImGui.TextColored(UIColors.Get("LightlessPurple"), "Available Tags"); @@ -798,10 +793,10 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase } else { - int pageCount = Math.Max(1, (totalAvailable + AvailableTagsPerPage - 1) / AvailableTagsPerPage); + int pageCount = Math.Max(1, (totalAvailable + _availableTagsPerPage - 1) / _availableTagsPerPage); _availableTagPage = Math.Clamp(_availableTagPage, 0, pageCount - 1); - int start = _availableTagPage * AvailableTagsPerPage; - int end = Math.Min(totalAvailable, start + AvailableTagsPerPage); + int start = _availableTagPage * _availableTagsPerPage; + int end = Math.Min(totalAvailable, start + _availableTagsPerPage); ImGui.SameLine(); ImGui.TextDisabled($"Page {_availableTagPage + 1}/{pageCount}"); @@ -1118,8 +1113,8 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase var monoFont = UiBuilder.MonoFont; using (ImRaii.PushFont(monoFont)) { - var previewTextColor = textEnabled ? textColor : Vector4.One; - var previewGlowColor = glowEnabled ? glowColor : Vector4.Zero; + var previewTextColor = _textEnabled ? _textColor : Vector4.One; + var previewGlowColor = _glowEnabled ? _glowColor : Vector4.Zero; var seString = SeStringUtils.BuildFormattedPlayerName(_apiController.DisplayName, previewTextColor, previewGlowColor); var drawList = ImGui.GetWindowDrawList(); @@ -1151,33 +1146,33 @@ public partial class EditProfileUi : WindowMediatorSubscriberBase if (!hasVanity) ImGui.BeginDisabled(); - if (DrawCheckboxRow("Enable custom text color", textEnabled, out var newTextEnabled)) - textEnabled = newTextEnabled; + if (DrawCheckboxRow("Enable custom text color", _textEnabled, out var newTextEnabled)) + _textEnabled = newTextEnabled; ImGui.SameLine(); - ImGui.BeginDisabled(!textEnabled); - ImGui.ColorEdit4("Text Color##vanityTextColor", ref textColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf); + ImGui.BeginDisabled(!_textEnabled); + ImGui.ColorEdit4("Text Color##vanityTextColor", ref _textColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf); ImGui.EndDisabled(); - if (DrawCheckboxRow("Enable glow color", glowEnabled, out var newGlowEnabled)) - glowEnabled = newGlowEnabled; + if (DrawCheckboxRow("Enable glow color", _glowEnabled, out var newGlowEnabled)) + _glowEnabled = newGlowEnabled; ImGui.SameLine(); - ImGui.BeginDisabled(!glowEnabled); - ImGui.ColorEdit4("Glow Color##vanityGlowColor", ref glowColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf); + ImGui.BeginDisabled(!_glowEnabled); + ImGui.ColorEdit4("Glow Color##vanityGlowColor", ref _glowColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.AlphaPreviewHalf); ImGui.EndDisabled(); - bool changed = !Equals(_savedVanity, new VanityState(textEnabled, glowEnabled, textColor, glowColor)); + bool changed = !Equals(_savedVanity, new VanityState(_textEnabled, _glowEnabled, _textColor, _glowColor)); if (!changed) ImGui.BeginDisabled(); if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Vanity Changes")) { - string? newText = textEnabled ? UIColors.RgbaToHex(textColor) : string.Empty; - string? newGlow = glowEnabled ? UIColors.RgbaToHex(glowColor) : string.Empty; + string? newText = _textEnabled ? UIColors.RgbaToHex(_textColor) : string.Empty; + string? newGlow = _glowEnabled ? UIColors.RgbaToHex(_glowColor) : string.Empty; _ = _apiController.UserUpdateVanityColors(new UserVanityColorsDto(newText, newGlow)); - _savedVanity = new VanityState(textEnabled, glowEnabled, textColor, glowColor); + _savedVanity = new VanityState(_textEnabled, _glowEnabled, _textColor, _glowColor); } if (!changed)