From b56813c1de2f6cbcb3e28d5f00e7c2ce97251e25 Mon Sep 17 00:00:00 2001 From: cake Date: Sat, 15 Nov 2025 04:46:07 +0100 Subject: [PATCH] Fixed many warnings, moved some classes to their own files. --- LightlessSync/Plugin.cs | 2 +- ...gService.cs => BroadcastScannerService.cs} | 0 LightlessSync/Services/BroadcastService.cs | 5 +- .../CharaData/CharacterAnalysisSummary.cs | 19 + .../Models/CharacterAnalysisObjectSummary.cs | 8 + LightlessSync/Services/CharacterAnalyzer.cs | 29 +- LightlessSync/Services/ContextMenuService.cs | 6 +- LightlessSync/Services/DalamudUtilService.cs | 12 +- .../Services/LightlessProfileManager.cs | 1 + LightlessSync/Services/NameplateHandler.cs | 1063 ++++++++--------- LightlessSync/Services/NameplateService.cs | 1 - LightlessSync/Services/NotificationService.cs | 45 +- .../PairProcessingLimiterSnapshot.cs | 9 + .../Services/PairProcessingLimiter.cs | 21 +- .../LightlessGroupProfileData.cs | 2 +- .../LightlessUserProfileData.cs | 2 +- LightlessSync/UI/DownloadUi.cs | 1 + LightlessSync/UI/EditProfileUi.cs | 2 - LightlessSync/UI/IntroUI.cs | 6 +- LightlessSync/UI/SyncshellAdminUI.cs | 2 +- 20 files changed, 622 insertions(+), 614 deletions(-) rename LightlessSync/Services/{BroadcastScanningService.cs => BroadcastScannerService.cs} (100%) create mode 100644 LightlessSync/Services/CharaData/CharacterAnalysisSummary.cs create mode 100644 LightlessSync/Services/CharaData/Models/CharacterAnalysisObjectSummary.cs create mode 100644 LightlessSync/Services/PairProcessing/PairProcessingLimiterSnapshot.cs rename LightlessSync/Services/{ => Profiles}/LightlessGroupProfileData.cs (84%) rename LightlessSync/Services/{ => Profiles}/LightlessUserProfileData.cs (90%) diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 19e2bd0..50b50cb 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -279,7 +279,7 @@ public sealed class Plugin : IDalamudPlugin s.GetRequiredService())); collection.AddScoped((s) => new NameplateService(s.GetRequiredService>(), s.GetRequiredService(), namePlateGui, clientState, s.GetRequiredService(), s.GetRequiredService())); - collection.AddScoped((s) => new NameplateHandler(s.GetRequiredService>(), addonLifecycle, gameGui, s.GetRequiredService(), + collection.AddScoped((s) => new NameplateHandler(s.GetRequiredService>(), addonLifecycle, gameGui, s.GetRequiredService(), s.GetRequiredService(), clientState, s.GetRequiredService())); collection.AddHostedService(p => p.GetRequiredService()); diff --git a/LightlessSync/Services/BroadcastScanningService.cs b/LightlessSync/Services/BroadcastScannerService.cs similarity index 100% rename from LightlessSync/Services/BroadcastScanningService.cs rename to LightlessSync/Services/BroadcastScannerService.cs diff --git a/LightlessSync/Services/BroadcastService.cs b/LightlessSync/Services/BroadcastService.cs index cca9af6..dfcd975 100644 --- a/LightlessSync/Services/BroadcastService.cs +++ b/LightlessSync/Services/BroadcastService.cs @@ -11,7 +11,6 @@ using LightlessSync.WebAPI; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using System.Threading; namespace LightlessSync.Services; public class BroadcastService : IHostedService, IMediatorSubscriber @@ -58,7 +57,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber { if (!_apiController.IsConnected) { - _logger.LogDebug(context + " skipped, not connected"); + _logger.LogDebug("{context} skipped, not connected", context); return; } await action().ConfigureAwait(false); @@ -372,7 +371,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber public async Task> AreUsersBroadcastingAsync(List hashedCids) { - Dictionary result = new(); + Dictionary result = new(StringComparer.Ordinal); await RequireConnectionAsync(nameof(AreUsersBroadcastingAsync), async () => { diff --git a/LightlessSync/Services/CharaData/CharacterAnalysisSummary.cs b/LightlessSync/Services/CharaData/CharacterAnalysisSummary.cs new file mode 100644 index 0000000..0eaf312 --- /dev/null +++ b/LightlessSync/Services/CharaData/CharacterAnalysisSummary.cs @@ -0,0 +1,19 @@ +using LightlessSync.API.Data.Enum; +using LightlessSync.Services.CharaData.Models; +using System.Collections.Immutable; +namespace LightlessSync.Services.CharaData; + +public sealed class CharacterAnalysisSummary +{ + public static CharacterAnalysisSummary Empty { get; } = + new(ImmutableDictionary.Empty); + + internal CharacterAnalysisSummary(IImmutableDictionary objects) + { + Objects = objects; + } + + public IImmutableDictionary Objects { get; } + + public bool HasData => Objects.Any(kvp => kvp.Value.HasEntries); +} \ No newline at end of file diff --git a/LightlessSync/Services/CharaData/Models/CharacterAnalysisObjectSummary.cs b/LightlessSync/Services/CharaData/Models/CharacterAnalysisObjectSummary.cs new file mode 100644 index 0000000..aa42394 --- /dev/null +++ b/LightlessSync/Services/CharaData/Models/CharacterAnalysisObjectSummary.cs @@ -0,0 +1,8 @@ +using System.Runtime.InteropServices; +namespace LightlessSync.Services.CharaData.Models; + +[StructLayout(LayoutKind.Auto)] +public readonly record struct CharacterAnalysisObjectSummary(int EntryCount, long TotalTriangles, long TexOriginalBytes, long TexCompressedBytes) +{ + public bool HasEntries => EntryCount > 0; +} diff --git a/LightlessSync/Services/CharacterAnalyzer.cs b/LightlessSync/Services/CharacterAnalyzer.cs index 27235f6..8b87c99 100644 --- a/LightlessSync/Services/CharacterAnalyzer.cs +++ b/LightlessSync/Services/CharacterAnalyzer.cs @@ -1,16 +1,14 @@ using LightlessSync.API.Data; using LightlessSync.API.Data.Enum; using LightlessSync.FileCache; +using LightlessSync.Services.CharaData; +using LightlessSync.Services.CharaData.Models; using LightlessSync.Services.Mediator; using LightlessSync.UI; using LightlessSync.Utils; using Lumina.Data.Files; using Microsoft.Extensions.Logging; -using System.Collections.Generic; using System.Collections.Immutable; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; namespace LightlessSync.Services; public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable @@ -99,6 +97,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable public void Dispose() { _analysisCts.CancelDispose(); + _baseAnalysisCts.Dispose(); } private async Task BaseAnalysis(CharacterData charaData, CancellationToken token) @@ -135,7 +134,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable { data[fileEntry.Hash] = new FileDataEntry(fileEntry.Hash, ext, [.. fileEntry.GamePaths], - fileCacheEntries.Select(c => c.ResolvedFilepath).Distinct().ToList(), + [.. fileCacheEntries.Select(c => c.ResolvedFilepath).Distinct(StringComparer.Ordinal)], entry.Size > 0 ? entry.Size.Value : 0, entry.CompressedSize > 0 ? entry.CompressedSize.Value : 0, tris); @@ -269,23 +268,3 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable }); } } - -public readonly record struct CharacterAnalysisObjectSummary(int EntryCount, long TotalTriangles, long TexOriginalBytes, long TexCompressedBytes) -{ - public bool HasEntries => EntryCount > 0; -} - -public sealed class CharacterAnalysisSummary -{ - public static CharacterAnalysisSummary Empty { get; } = - new(ImmutableDictionary.Empty); - - internal CharacterAnalysisSummary(IImmutableDictionary objects) - { - Objects = objects; - } - - public IImmutableDictionary Objects { get; } - - public bool HasData => Objects.Any(kvp => kvp.Value.HasEntries); -} \ No newline at end of file diff --git a/LightlessSync/Services/ContextMenuService.cs b/LightlessSync/Services/ContextMenuService.cs index 464fee1..4d50074 100644 --- a/LightlessSync/Services/ContextMenuService.cs +++ b/LightlessSync/Services/ContextMenuService.cs @@ -95,7 +95,7 @@ internal class ContextMenuService : IHostedService //Check if it is a real target. IPlayerCharacter? targetData = GetPlayerFromObjectTable(target); - if (targetData == null || targetData.Address == nint.Zero) + if (targetData == null || targetData.Address == nint.Zero || _clientState.LocalPlayer == null) return; //Check if user is directly paired or is own. @@ -120,7 +120,7 @@ internal class ContextMenuService : IHostedService PrefixChar = 'L', UseDefaultPrefix = false, PrefixColor = 708, - OnClicked = async _ => await HandleSelection(args).ConfigureAwait(false) + OnClicked = _ => HandleSelection(args).ConfigureAwait(false).GetAwaiter().GetResult() }); } @@ -200,8 +200,6 @@ internal class ContextMenuService : IHostedService private static bool IsChineseJapaneseKoreanCharacter(char c) => c >= 0x4E00 && c <= 0x9FFF; - public bool IsWorldValid(uint worldId) => IsWorldValid(GetWorld(worldId)); - public static bool IsWorldValid(World world) { var name = world.Name.ToString(); diff --git a/LightlessSync/Services/DalamudUtilService.cs b/LightlessSync/Services/DalamudUtilService.cs index e5fd735..f3bf08d 100644 --- a/LightlessSync/Services/DalamudUtilService.cs +++ b/LightlessSync/Services/DalamudUtilService.cs @@ -140,14 +140,14 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber public bool IsWine { get; init; } - public unsafe GameObject* GposeTarget + public static unsafe GameObject* GposeTarget { get => TargetSystem.Instance()->GPoseTarget; set => TargetSystem.Instance()->GPoseTarget = value; } - private unsafe bool HasGposeTarget => GposeTarget != null; - private unsafe int GPoseTargetIdx => !HasGposeTarget ? -1 : GposeTarget->ObjectIndex; + private static unsafe bool HasGposeTarget => GposeTarget != null; + private static unsafe int GPoseTargetIdx => !HasGposeTarget ? -1 : GposeTarget->ObjectIndex; public async Task GetGposeTargetGameObjectAsync() { @@ -513,15 +513,11 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber { logger.LogTrace("[{redrawId}] Waiting for {handler} to finish drawing", redrawId, handler); curWaitTime += tick; - await Task.Delay(tick).ConfigureAwait(true); + await Task.Delay(tick, ct.Value).ConfigureAwait(true); } logger.LogTrace("[{redrawId}] Finished drawing after {curWaitTime}ms", redrawId, curWaitTime); } - catch (NullReferenceException ex) - { - logger.LogWarning(ex, "Error accessing {handler}, object does not exist anymore?", handler); - } catch (AccessViolationException ex) { logger.LogWarning(ex, "Error accessing {handler}, object does not exist anymore?", handler); diff --git a/LightlessSync/Services/LightlessProfileManager.cs b/LightlessSync/Services/LightlessProfileManager.cs index 00b610b..a324a9a 100644 --- a/LightlessSync/Services/LightlessProfileManager.cs +++ b/LightlessSync/Services/LightlessProfileManager.cs @@ -2,6 +2,7 @@ using LightlessSync.API.Data.Comparer; using LightlessSync.LightlessConfiguration; using LightlessSync.Services.Mediator; +using LightlessSync.Services.Profiles; using LightlessSync.WebAPI; using Microsoft.Extensions.Logging; using Serilog.Core; diff --git a/LightlessSync/Services/NameplateHandler.cs b/LightlessSync/Services/NameplateHandler.cs index 11af974..5b2246f 100644 --- a/LightlessSync/Services/NameplateHandler.cs +++ b/LightlessSync/Services/NameplateHandler.cs @@ -16,7 +16,6 @@ using LightlessSync.UtilsEnum.Enum; // Created using https://github.com/PunishedPineapple/Distance as a reference, thank you! using Microsoft.Extensions.Logging; -using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; @@ -28,7 +27,6 @@ public unsafe class NameplateHandler : IMediatorSubscriber private readonly IAddonLifecycle _addonLifecycle; private readonly IGameGui _gameGui; private readonly IClientState _clientState; - private readonly DalamudUtilService _dalamudUtil; private readonly LightlessConfigService _configService; private readonly PairManager _pairManager; private readonly LightlessMediator _mediator; @@ -46,17 +44,15 @@ public unsafe class NameplateHandler : IMediatorSubscriber internal const uint mNameplateNodeIDBase = 0x7D99D500; private const string DefaultLabelText = "LightFinder"; private const SeIconChar DefaultIcon = SeIconChar.Hyadelyn; - private const int _containerOffsetX = 50; private static readonly string DefaultIconGlyph = SeIconCharExtensions.ToIconString(DefaultIcon); private ImmutableHashSet _activeBroadcastingCids = []; - public NameplateHandler(ILogger logger, IAddonLifecycle addonLifecycle, IGameGui gameGui, DalamudUtilService dalamudUtil, LightlessConfigService configService, LightlessMediator mediator, IClientState clientState, PairManager pairManager) + public NameplateHandler(ILogger logger, IAddonLifecycle addonLifecycle, IGameGui gameGui, LightlessConfigService configService, LightlessMediator mediator, IClientState clientState, PairManager pairManager) { _logger = logger; _addonLifecycle = addonLifecycle; _gameGui = gameGui; - _dalamudUtil = dalamudUtil; _configService = configService; _mediator = mediator; _clientState = clientState; @@ -118,577 +114,580 @@ public unsafe class NameplateHandler : IMediatorSubscriber { if (args.Addon.Address == nint.Zero) { - _logger.LogWarning("Nameplate draw detour received a null addon address, skipping update."); + 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() + if (_mpNameplateAddon != pNameplateAddon) { - 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) - { - _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) - { - _logger.LogWarning("Skipping nameplate node destroy due to addon address mismatch (cached {Cached:X}, current {Current:X}).", (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)) - { - _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(true); - _mTextNodes[i] = null; - } - catch (Exception e) - { - _logger.LogError($"Unknown error while removing text node 0x{(IntPtr)pTextNode:X} for nameplate {i} on component node 0x{(IntPtr)pNameplateNode:X}:\n{e}"); - } - } - } - + 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(); } - private void HideAllNameplateNodes() + UpdateNameplateNodes(); +} + +private void CreateNameplateNodes() +{ + for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i) { - for (int i = 0; i < _mTextNodes.Length; ++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) { - HideNameplateTextNode(i); + 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); + } } } - private void UpdateNameplateNodes() + 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) { - var currentHandle = _gameGui.GetAddonByName("NamePlate"); - if (currentHandle.Address == nint.Zero) - { + 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; - } + return; + } - var currentAddon = (AddonNamePlate*)currentHandle.Address; - if (_mpNameplateAddon == null || currentAddon == null || currentAddon != _mpNameplateAddon) - { - if (_mpNameplateAddon != null) - _logger.LogDebug("Cached NamePlate addon pointer differs from current: waiting for new hook (cached {Cached:X}, current {Current:X}).", (IntPtr)_mpNameplateAddon, (IntPtr)currentAddon); - 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) - { + var framework = Framework.Instance(); + if (framework == null) + { + if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("Framework instance unavailable during nameplate update, skipping."); - return; - } + return; + } - var uiModule = framework->GetUIModule(); - if (uiModule == null) - { + var uiModule = framework->GetUIModule(); + if (uiModule == null) + { + if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("UI module unavailable during nameplate update, skipping."); - return; - } + return; + } - var ui3DModule = uiModule->GetUI3DModule(); - if (ui3DModule == null) - { + var ui3DModule = uiModule->GetUI3DModule(); + if (ui3DModule == null) + { + if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("UI3D module unavailable during nameplate update, skipping."); - return; + 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; } - 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) + // CID gating + var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)gameObject); + if (cid == null || !_activeBroadcastingCids.Contains(cid)) { - 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 = _clientState.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 if (_cachedNameplateTextOffsets[nameplateIndex] != int.MinValue) - { - textOffset = _cachedNameplateTextOffsets[nameplateIndex]; - } - 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; + pNode->AtkResNode.ToggleVisibility(enable: false); + continue; } - } - 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) + var local = _clientState.LocalPlayer; + if (!config.LightfinderLabelShowOwn && local != null && + objectInfo->GameObject->GetGameObjectId() == local.GameObjectId) { - pNode->AtkResNode.ToggleVisibility(false); + pNode->AtkResNode.ToggleVisibility(enable: false); + continue; } - } - private AddonNamePlate.NamePlateObject? GetNameplateObject(int i) - { - if (i < AddonNamePlate.NumNamePlateObjects && - _mpNameplateAddon != null && - _mpNameplateAddon->NamePlateObjectArray[i].RootComponentNode != null) + var hidePaired = !config.LightfinderLabelShowPaired; + + var goId = (ulong)gameObject->GetGameObjectId(); + if (hidePaired && visibleUserIdsSnapshot.Contains(goId)) { - return _mpNameplateAddon->NamePlateObjectArray[i]; + 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 { - return null; + blockHeight = _cachedNameplateTextHeights[nameplateIndex]; } - } - private AtkComponentNode* GetNameplateComponentNode(int i) - { - var nameplateObject = GetNameplateObject(i); - return nameplateObject != null ? nameplateObject.Value.RootComponentNode : null; - } - - private HashSet VisibleUserIds => [.. _pairManager.GetOnlineUserPairs() - .Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue) - .Select(u => (ulong)u.PlayerCharacterId)]; - - - public void FlagRefresh() - { - _needsLabelRefresh = true; - } - - public void OnTick(PriorityFrameworkUpdateMessage _) - { - if (_needsLabelRefresh) + if (blockHeight <= 0) { - UpdateNameplateNodes(); - _needsLabelRefresh = false; + blockHeight = GetScaledTextHeight(nameText); + if (blockHeight <= 0) + blockHeight = nodeHeight; + + _cachedNameplateTextHeights[nameplateIndex] = blockHeight; } - } - public void UpdateBroadcastingCids(IEnumerable cids) - { - var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal); - if (ReferenceEquals(_activeBroadcastingCids, newSet) || _activeBroadcastingCids.SetEquals(newSet)) - return; + var containerHeight = (int)nameContainer->Height; + if (containerHeight > 0) + { + _cachedNameplateContainerHeights[nameplateIndex] = containerHeight; + } + else + { + containerHeight = _cachedNameplateContainerHeights[nameplateIndex]; + } - _activeBroadcastingCids = newSet; - _logger.LogInformation("Active broadcast CIDs: {Cids}", string.Join(',', _activeBroadcastingCids)); - FlagRefresh(); - } + if (containerHeight <= 0) + { + containerHeight = blockHeight + (int)System.Math.Round(8 * textScaleY); + if (containerHeight <= blockHeight) + containerHeight = blockHeight + 1; - 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); + _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 => [.. _pairManager.GetOnlineUserPairs() + .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/Services/NameplateService.cs b/LightlessSync/Services/NameplateService.cs index 8ccc362..0961663 100644 --- a/LightlessSync/Services/NameplateService.cs +++ b/LightlessSync/Services/NameplateService.cs @@ -47,7 +47,6 @@ public class NameplateService : DisposableMediatorSubscriberBase .Select(u => (ulong)u.PlayerCharacterId) .ToHashSet(); - var now = DateTime.UtcNow; var colors = _configService.Current.NameplateColors; foreach (var handler in handlers) diff --git a/LightlessSync/Services/NotificationService.cs b/LightlessSync/Services/NotificationService.cs index 8709710..cbe0ee4 100644 --- a/LightlessSync/Services/NotificationService.cs +++ b/LightlessSync/Services/NotificationService.cs @@ -23,7 +23,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ private readonly INotificationManager _notificationManager; private readonly IChatGui _chatGui; private readonly PairRequestService _pairRequestService; - private readonly HashSet _shownPairRequestNotifications = new(); + private readonly HashSet _shownPairRequestNotifications = []; public NotificationService( ILogger logger, @@ -59,7 +59,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ { var notification = CreateNotification(title, message, type, duration, actions, soundEffectId); - if (_configService.Current.AutoDismissOnAction && notification.Actions.Any()) + if (_configService.Current.AutoDismissOnAction && notification.Actions.Count != 0) { WrapActionsWithAutoDismiss(notification); } @@ -104,7 +104,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ } } - private void DismissNotification(LightlessNotification notification) + private static void DismissNotification(LightlessNotification notification) { notification.IsDismissed = true; notification.IsAnimatingOut = true; @@ -208,10 +208,12 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ Mediator.Publish(new LightlessNotificationMessage(notification)); } - private string FormatDownloadCompleteMessage(string fileName, int fileCount) => - fileCount > 1 + private static string FormatDownloadCompleteMessage(string fileName, int fileCount) + { + return fileCount > 1 ? $"Downloaded {fileCount} files successfully." : $"Downloaded {fileName} successfully."; + } private List CreateDownloadCompleteActions(Action? onOpenFolder) { @@ -257,8 +259,10 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ Mediator.Publish(new LightlessNotificationMessage(notification)); } - private string FormatErrorMessage(string message, Exception? exception) => - exception != null ? $"{message}\n\nError: {exception.Message}" : message; + private static string FormatErrorMessage(string message, Exception? exception) + { + return exception != null ? $"{message}\n\nError: {exception.Message}" : message; + } private List CreateErrorActions(Action? onRetry, Action? onViewLog) { @@ -332,8 +336,9 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ return string.Join("\n", activeDownloads.Select(x => $"• {x.PlayerName}: {FormatDownloadStatus(x)}")); } - private string FormatDownloadStatus((string PlayerName, float Progress, string Status) download) => - download.Status switch + private static string FormatDownloadStatus((string PlayerName, float Progress, string Status) download) + { + return download.Status switch { "downloading" => $"{download.Progress:P0}", "decompressing" => "decompressing", @@ -341,6 +346,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ "waiting" => "waiting for slot", _ => download.Status }; + } private TimeSpan GetDefaultDurationForType(NotificationType type) => type switch { @@ -478,13 +484,16 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ }); } - private Dalamud.Interface.ImGuiNotification.NotificationType - ConvertToDalamudNotificationType(NotificationType type) => type switch + private static Dalamud.Interface.ImGuiNotification.NotificationType + ConvertToDalamudNotificationType(NotificationType type) { - NotificationType.Error => Dalamud.Interface.ImGuiNotification.NotificationType.Error, - NotificationType.Warning => Dalamud.Interface.ImGuiNotification.NotificationType.Warning, - _ => Dalamud.Interface.ImGuiNotification.NotificationType.Info - }; + return type switch + { + NotificationType.Error => Dalamud.Interface.ImGuiNotification.NotificationType.Error, + NotificationType.Warning => Dalamud.Interface.ImGuiNotification.NotificationType.Warning, + _ => Dalamud.Interface.ImGuiNotification.NotificationType.Info + }; + } private void ShowChat(NotificationMessage msg) { @@ -568,7 +577,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ private void HandlePairRequestsUpdated(PairRequestsUpdatedMessage _) { var activeRequests = _pairRequestService.GetActiveRequests(); - var activeRequestIds = activeRequests.Select(r => r.HashedCid).ToHashSet(); + var activeRequestIds = activeRequests.Select(r => r.HashedCid).ToHashSet(StringComparer.Ordinal); // Dismiss notifications for requests that are no longer active (expired) var notificationsToRemove = _shownPairRequestNotifications @@ -585,7 +594,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ private void HandlePairDownloadStatus(PairDownloadStatusMessage msg) { - var userDownloads = msg.DownloadStatus.Where(x => x.PlayerName != "Pair Queue").ToList(); + var userDownloads = msg.DownloadStatus.Where(x => !string.Equals(x.PlayerName, "Pair Queue", StringComparison.Ordinal)).ToList(); var totalProgress = userDownloads.Count > 0 ? userDownloads.Average(x => x.Progress) : 0f; var message = BuildPairDownloadMessage(userDownloads, msg.QueueWaiting); @@ -734,7 +743,7 @@ public class NotificationService : DisposableMediatorSubscriberBase, IHostedServ return actions; } - private string GetUserDisplayName(UserData userData, string playerName) + private static string GetUserDisplayName(UserData userData, string playerName) { if (!string.IsNullOrEmpty(userData.Alias) && !string.Equals(userData.Alias, userData.UID, StringComparison.Ordinal)) { diff --git a/LightlessSync/Services/PairProcessing/PairProcessingLimiterSnapshot.cs b/LightlessSync/Services/PairProcessing/PairProcessingLimiterSnapshot.cs new file mode 100644 index 0000000..64cc6b0 --- /dev/null +++ b/LightlessSync/Services/PairProcessing/PairProcessingLimiterSnapshot.cs @@ -0,0 +1,9 @@ +using System.Runtime.InteropServices; + +namespace LightlessSync.Services.PairProcessing; + +[StructLayout(LayoutKind.Auto)] +public readonly record struct PairProcessingLimiterSnapshot(bool IsEnabled, int Limit, int InFlight, int Waiting) +{ + public int Remaining => Math.Max(0, Limit - InFlight); +} diff --git a/LightlessSync/Services/PairProcessingLimiter.cs b/LightlessSync/Services/PairProcessingLimiter.cs index 239ba75..35b6d1c 100644 --- a/LightlessSync/Services/PairProcessingLimiter.cs +++ b/LightlessSync/Services/PairProcessingLimiter.cs @@ -1,15 +1,13 @@ -using System; -using System.Threading; -using System.Threading.Tasks; using LightlessSync.LightlessConfiguration; using LightlessSync.Services.Mediator; +using LightlessSync.Services.PairProcessing; using Microsoft.Extensions.Logging; namespace LightlessSync.Services; public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase { - private const int HardLimit = 32; + private const int _hardLimit = 32; private readonly LightlessConfigService _configService; private readonly object _limitLock = new(); private readonly SemaphoreSlim _semaphore; @@ -24,8 +22,8 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase { _configService = configService; _currentLimit = CalculateLimit(); - var initialCount = _configService.Current.EnablePairProcessingLimiter ? _currentLimit : HardLimit; - _semaphore = new SemaphoreSlim(initialCount, HardLimit); + var initialCount = _configService.Current.EnablePairProcessingLimiter ? _currentLimit : _hardLimit; + _semaphore = new SemaphoreSlim(initialCount, _hardLimit); Mediator.Subscribe(this, _ => UpdateSemaphoreLimit()); } @@ -88,7 +86,7 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase if (!enabled) { - var releaseAmount = HardLimit - _semaphore.CurrentCount; + var releaseAmount = _hardLimit - _semaphore.CurrentCount; if (releaseAmount > 0) { TryReleaseSemaphore(releaseAmount); @@ -110,7 +108,7 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase var increment = desiredLimit - _currentLimit; _pendingIncrements += increment; - var available = HardLimit - _semaphore.CurrentCount; + var available = _hardLimit - _semaphore.CurrentCount; var toRelease = Math.Min(_pendingIncrements, available); if (toRelease > 0 && TryReleaseSemaphore(toRelease)) { @@ -148,7 +146,7 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase private int CalculateLimit() { var configured = _configService.Current.MaxConcurrentPairApplications; - return Math.Clamp(configured, 1, HardLimit); + return Math.Clamp(configured, 1, _hardLimit); } private bool TryReleaseSemaphore(int count = 1) @@ -248,8 +246,3 @@ public sealed class PairProcessingLimiter : DisposableMediatorSubscriberBase } } } - -public readonly record struct PairProcessingLimiterSnapshot(bool IsEnabled, int Limit, int InFlight, int Waiting) -{ - public int Remaining => Math.Max(0, Limit - InFlight); -} diff --git a/LightlessSync/Services/LightlessGroupProfileData.cs b/LightlessSync/Services/Profiles/LightlessGroupProfileData.cs similarity index 84% rename from LightlessSync/Services/LightlessGroupProfileData.cs rename to LightlessSync/Services/Profiles/LightlessGroupProfileData.cs index 1b27b40..1a6d212 100644 --- a/LightlessSync/Services/LightlessGroupProfileData.cs +++ b/LightlessSync/Services/Profiles/LightlessGroupProfileData.cs @@ -1,4 +1,4 @@ -namespace LightlessSync.Services; +namespace LightlessSync.Services.Profiles; public record LightlessGroupProfileData(string Base64ProfilePicture, string Description, int[] Tags, bool IsNsfw, bool IsDisabled) { diff --git a/LightlessSync/Services/LightlessUserProfileData.cs b/LightlessSync/Services/Profiles/LightlessUserProfileData.cs similarity index 90% rename from LightlessSync/Services/LightlessUserProfileData.cs rename to LightlessSync/Services/Profiles/LightlessUserProfileData.cs index 3319043..ebad3fe 100644 --- a/LightlessSync/Services/LightlessUserProfileData.cs +++ b/LightlessSync/Services/Profiles/LightlessUserProfileData.cs @@ -1,4 +1,4 @@ -namespace LightlessSync.Services; +namespace LightlessSync.Services.Profiles; public record LightlessUserProfileData(bool IsFlagged, bool IsNSFW, string Base64ProfilePicture, string Base64SupporterPicture, string Description) { diff --git a/LightlessSync/UI/DownloadUi.cs b/LightlessSync/UI/DownloadUi.cs index aa3132a..337fb41 100644 --- a/LightlessSync/UI/DownloadUi.cs +++ b/LightlessSync/UI/DownloadUi.cs @@ -5,6 +5,7 @@ using LightlessSync.LightlessConfiguration.Models; using LightlessSync.PlayerData.Handlers; using LightlessSync.Services; using LightlessSync.Services.Mediator; +using LightlessSync.Services.PairProcessing; using LightlessSync.WebAPI.Files; using LightlessSync.WebAPI.Files.Models; using Microsoft.Extensions.Logging; diff --git a/LightlessSync/UI/EditProfileUi.cs b/LightlessSync/UI/EditProfileUi.cs index 0588797..880a0d2 100644 --- a/LightlessSync/UI/EditProfileUi.cs +++ b/LightlessSync/UI/EditProfileUi.cs @@ -35,7 +35,6 @@ public class EditProfileUi : WindowMediatorSubscriberBase private bool _wasOpen; private Vector4 _currentBg = new(0.15f, 0.15f, 0.15f, 1f); - private bool vanityInitialized; // useless for now private bool textEnabled; private bool glowEnabled; private Vector4 textColor; @@ -86,7 +85,6 @@ public class EditProfileUi : WindowMediatorSubscriberBase glowColor = glowEnabled ? UIColors.HexToRgba(_apiController.TextGlowColorHex!) : Vector4.Zero; _savedVanity = new VanityState(textEnabled, glowEnabled, textColor, glowColor); - vanityInitialized = true; } protected override void DrawInternal() diff --git a/LightlessSync/UI/IntroUI.cs b/LightlessSync/UI/IntroUI.cs index 470cadb..97935c2 100644 --- a/LightlessSync/UI/IntroUI.cs +++ b/LightlessSync/UI/IntroUI.cs @@ -267,7 +267,7 @@ public partial class IntroUi : WindowMediatorSubscriberBase { UiSharedService.ColorTextWrapped("Your secret key must be exactly 64 characters long. Don't enter your Lodestone auth here.", ImGuiColors.DalamudRed); } - else if (_secretKey.Length == 64 && !HexRegex().IsMatch(_secretKey)) + else if (_secretKey.Length == 64 && !SecretRegex().IsMatch(_secretKey)) { UiSharedService.ColorTextWrapped("Your secret key can only contain ABCDEF and the numbers 0-9.", ImGuiColors.DalamudRed); } @@ -360,6 +360,6 @@ public partial class IntroUi : WindowMediatorSubscriberBase _tosParagraphs = [Strings.ToS.Paragraph1, Strings.ToS.Paragraph2, Strings.ToS.Paragraph3, Strings.ToS.Paragraph4, Strings.ToS.Paragraph5, Strings.ToS.Paragraph6]; } - [GeneratedRegex("^([A-F0-9]{2})+")] - private static partial Regex HexRegex(); + [GeneratedRegex("^[A-F0-9]{64}$", RegexOptions.Compiled | RegexOptions.CultureInvariant)] + private static partial Regex SecretRegex(); } \ No newline at end of file diff --git a/LightlessSync/UI/SyncshellAdminUI.cs b/LightlessSync/UI/SyncshellAdminUI.cs index 0967290..7497e78 100644 --- a/LightlessSync/UI/SyncshellAdminUI.cs +++ b/LightlessSync/UI/SyncshellAdminUI.cs @@ -13,13 +13,13 @@ using LightlessSync.API.Dto.User; using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; using LightlessSync.Services.Mediator; +using LightlessSync.Services.Profiles; using LightlessSync.UI.Handlers; using LightlessSync.WebAPI; using Microsoft.Extensions.Logging; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using System.Globalization; -using System.Linq; using System.Numerics; namespace LightlessSync.UI;