diff --git a/LightlessSync/FileCache/CacheMonitor.cs b/LightlessSync/FileCache/CacheMonitor.cs index 8b90013..d32b40d 100644 --- a/LightlessSync/FileCache/CacheMonitor.cs +++ b/LightlessSync/FileCache/CacheMonitor.cs @@ -259,6 +259,9 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase private CancellationTokenSource _penumbraFswCts = new(); private CancellationTokenSource _lightlessFswCts = new(); + private long totalSize; + private long maxCacheBytes; + public FileSystemWatcher? PenumbraWatcher { get; private set; } public FileSystemWatcher? LightlessWatcher { get; private set; } @@ -485,7 +488,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase { long size = 0; - if (!isWine) + if (!_dalamudUtil.IsWine) { try { diff --git a/LightlessSync/FileCache/FileCompactor.cs b/LightlessSync/FileCache/FileCompactor.cs index 971aa15..771f558 100644 --- a/LightlessSync/FileCache/FileCompactor.cs +++ b/LightlessSync/FileCache/FileCompactor.cs @@ -17,7 +17,6 @@ public sealed partial class FileCompactor : IDisposable public const uint FSCTL_DELETE_EXTERNAL_BACKING = 0x90314U; public const ulong WOF_PROVIDER_FILE = 2UL; public const int _maxRetries = 3; - private readonly bool _isWindows; private readonly ConcurrentDictionary _pendingCompactions; private readonly ILogger _logger; @@ -272,9 +271,6 @@ public sealed partial class FileCompactor : IDisposable ? RunProcessShell($"stat -c='%b' {QuoteSingle(linuxPath)}", workingDir: null, 10000) : RunProcessDirect("stat", ["-c='%b'", linuxPath], workingDir: null, 10000); - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("Btrfs size probe failed for {linux} (exit {code}). stdout='{so}' stderr='{se}'. Falling back to Length.", linuxPath, res.code, outTrim, (res.se ?? "").Trim()); - return (flowControl: false, value: fileInfo.Length); } catch (Exception ex) @@ -1160,6 +1156,11 @@ public sealed partial class FileCompactor : IDisposable } return true; } + catch (Exception ex) + { + _logger.LogTrace(ex, "Probe open failed for {file} (linux={linux})", winePath, linuxPath); + return false; + } } /// diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 5136a6e..6e68f77 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -288,7 +288,7 @@ public sealed class Plugin : IDalamudPlugin clientState, sp.GetRequiredService())); collection.AddSingleton(); - collection.AddSingleton(s => new BroadcastScannerService( s.GetRequiredService>(), framework, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); + collection.AddSingleton(s => new BroadcastScannerService(s.GetRequiredService>(), framework, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); // add scoped services @@ -342,7 +342,7 @@ public sealed class Plugin : IDalamudPlugin 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(), + 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/BroadcastScannerService.cs b/LightlessSync/Services/BroadcastScannerService.cs index b6e82e1..96576d1 100644 --- a/LightlessSync/Services/BroadcastScannerService.cs +++ b/LightlessSync/Services/BroadcastScannerService.cs @@ -21,16 +21,16 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase private readonly HashSet _lookupQueuedCids = []; private readonly HashSet _syncshellCids = []; - private static readonly TimeSpan MaxAllowedTtl = TimeSpan.FromMinutes(4); - private static readonly TimeSpan RetryDelay = TimeSpan.FromMinutes(1); + private static readonly TimeSpan _maxAllowedTtl = TimeSpan.FromMinutes(4); + private static readonly TimeSpan _retryDelay = TimeSpan.FromMinutes(1); private readonly CancellationTokenSource _cleanupCts = new(); private readonly Task? _cleanupTask; private readonly int _checkEveryFrames = 20; private int _frameCounter = 0; - private const int MaxLookupsPerFrame = 30; - private const int MaxQueueSize = 100; + private const int _maxLookupsPerFrame = 30; + private const int _maxQueueSize = 100; private volatile bool _batchRunning = false; @@ -38,11 +38,11 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase public readonly record struct BroadcastEntry(bool IsBroadcasting, DateTime ExpiryTime, string? GID); public BroadcastScannerService(ILogger logger, - IObjectTable objectTable, IFramework framework, BroadcastService broadcastService, LightlessMediator mediator, - NameplateHandler nameplateHandler) : base(logger, mediator) + NameplateHandler nameplateHandler, + ActorObjectService actorTracker) : base(logger, mediator) { _logger = logger; _actorTracker = actorTracker; @@ -57,6 +57,7 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase _cleanupTask = Task.Run(ExpiredBroadcastCleanupLoop); _nameplateHandler.Init(); + _actorTracker = actorTracker; } private void OnFrameworkUpdate(IFramework framework) => Update(); @@ -79,14 +80,14 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer(address); var isStale = !_broadcastCache.TryGetValue(cid, out var entry) || entry.ExpiryTime <= now; - if (isStale && _lookupQueuedCids.Add(cid) && _lookupQueue.Count < MaxQueueSize) + if (isStale && _lookupQueuedCids.Add(cid) && _lookupQueue.Count < _maxQueueSize) _lookupQueue.Enqueue(cid); } if (_frameCounter % _checkEveryFrames == 0 && _lookupQueue.Count > 0) { var cidsToLookup = new List(); - while (_lookupQueue.Count > 0 && lookupsThisFrame < MaxLookupsPerFrame) + while (_lookupQueue.Count > 0 && lookupsThisFrame < _maxLookupsPerFrame) { var cid = _lookupQueue.Dequeue(); _lookupQueuedCids.Remove(cid); @@ -113,8 +114,8 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase continue; var ttl = info.IsBroadcasting && info.TTL.HasValue - ? TimeSpan.FromTicks(Math.Min(info.TTL.Value.Ticks, MaxAllowedTtl.Ticks)) - : RetryDelay; + ? TimeSpan.FromTicks(Math.Min(info.TTL.Value.Ticks, _maxAllowedTtl.Ticks)) + : _retryDelay; var expiry = now + ttl; diff --git a/LightlessSync/Services/NameplateHandler.cs b/LightlessSync/Services/NameplateHandler.cs index ccf7ae9..f117da9 100644 --- a/LightlessSync/Services/NameplateHandler.cs +++ b/LightlessSync/Services/NameplateHandler.cs @@ -48,7 +48,7 @@ public unsafe class NameplateHandler : IMediatorSubscriber private ImmutableHashSet _activeBroadcastingCids = []; - public NameplateHandler(ILogger logger, IAddonLifecycle addonLifecycle, IGameGui gameGui, LightlessConfigService configService, LightlessMediator mediator, IClientState clientState, PairManager pairManager) + public NameplateHandler(ILogger logger, IAddonLifecycle addonLifecycle, IGameGui gameGui, LightlessConfigService configService, LightlessMediator mediator, IClientState clientState, PairUiService pairUiService) { _logger = logger; _addonLifecycle = addonLifecycle; @@ -121,573 +121,573 @@ public unsafe class NameplateHandler : IMediatorSubscriber var pNameplateAddon = (AddonNamePlate*)args.Addon.Address; - if (_mpNameplateAddon != pNameplateAddon) + 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 < _mTextNodes.Length; ++i) _mTextNodes[i] = null; + 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); - _mpNameplateAddon = pNameplateAddon; - if (_mpNameplateAddon != null) CreateNameplateNodes(); } - UpdateNameplateNodes(); -} - -private void CreateNameplateNodes() -{ - for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i) + private void HideAllNameplateNodes() { - 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) + for (int i = 0; i < _mTextNodes.Length; ++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; + HideNameplateTextNode(i); } } -} -private void DestroyNameplateNodes() -{ - var currentHandle = _gameGui.GetAddonByName("NamePlate", 1); - if (currentHandle.Address == nint.Zero) + private void UpdateNameplateNodes() { - 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)) + var currentHandle = _gameGui.GetAddonByName("NamePlate"); + if (currentHandle.Address == nint.Zero) { if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("Skipping destroy for nameplate {Index} because its component node is unavailable.", i); - continue; + _logger.LogDebug("NamePlate addon unavailable during update, skipping label refresh."); + return; } - if (pTextNode != null && pNameplateNode != null && pNameplateNode->Component != null) + var currentAddon = (AddonNamePlate*)currentHandle.Address; + if (_mpNameplateAddon == null || currentAddon == null || currentAddon != _mpNameplateAddon) { - try + 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) { - 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; + pNode->AtkResNode.ToggleVisibility(enable: false); + continue; } - catch (Exception e) + + // CID gating + var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)gameObject); + if (cid == null || !_activeBroadcastingCids.Contains(cid)) { - 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); + pNode->AtkResNode.ToggleVisibility(enable: false); + continue; } - } - } - 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); -} + var local = _clientState.LocalPlayer; + if (!config.LightfinderLabelShowOwn && local != null && + objectInfo->GameObject->GetGameObjectId() == local.GameObjectId) + { + pNode->AtkResNode.ToggleVisibility(enable: false); + continue; + } -private void HideAllNameplateNodes() -{ - for (int i = 0; i < _mTextNodes.Length; ++i) - { - HideNameplateTextNode(i); - } -} + var hidePaired = !config.LightfinderLabelShowPaired; -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 goId = (ulong)gameObject->GetGameObjectId(); + if (hidePaired && visibleUserIdsSnapshot.Contains(goId)) + { + pNode->AtkResNode.ToggleVisibility(enable: false); + continue; + } - 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 nameplateObject = _mpNameplateAddon->NamePlateObjectArray[nameplateIndex]; + var root = nameplateObject.RootComponentNode; + var nameContainer = nameplateObject.NameContainer; + var nameText = nameplateObject.NameText; + var marker = nameplateObject.MarkerIcon; - var framework = Framework.Instance(); - if (framework == null) - { - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("Framework instance unavailable during nameplate update, skipping."); - return; - } + 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; + } - var uiModule = framework->GetUIModule(); - if (uiModule == null) - { - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("UI module unavailable during nameplate update, skipping."); - return; - } + root->Component->UldManager.UpdateDrawNodeList(); - var ui3DModule = uiModule->GetUI3DModule(); - if (ui3DModule == null) - { - if (_logger.IsEnabled(LogLevel.Debug)) - _logger.LogDebug("UI3D module unavailable during nameplate update, skipping."); - return; - } + bool isVisible = + ((marker != null) && marker->AtkResNode.IsVisible()) || + (nameContainer->IsVisible() && nameText->AtkResNode.IsVisible()) || + config.LightfinderLabelShowHidden; - var vec = ui3DModule->NamePlateObjectInfoPointers; - if (vec.IsEmpty) - return; + pNode->AtkResNode.ToggleVisibility(isVisible); + if (!isVisible) + continue; - var visibleUserIdsSnapshot = VisibleUserIds; + var labelColor = UIColors.Get("Lightfinder"); + var edgeColor = UIColors.Get("LightfinderEdge"); - var safeCount = System.Math.Min( - ui3DModule->NamePlateObjectInfoCount, - vec.Length - ); + 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; - 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 = _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 - { - 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(); + 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); - pNode->AtkResNode.Width = (ushort)nodeWidth; - } - else - { - pNode->TextFlags |= TextFlags.AutoAdjustNodeSize; - pNode->AtkResNode.Width = 0; - nodeWidth = pNode->AtkResNode.GetWidth(); - } + 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; - 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) + var blockHeight = System.Math.Abs((int)nameplateObject.TextH); + if (blockHeight > 0) { - 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; + _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; } - 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; + 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 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 scale = resNode->ScaleY; + if (scale <= 0f) + scale = 1f; - var computed = (int)System.Math.Round(rawHeight * scale); - return System.Math.Max(1, computed); -} + 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; + 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 resNode = &node->AtkResNode; + var rawWidth = (int)resNode->GetWidth(); + if (rawWidth <= 0) + rawWidth = AtkNodeHelpers.DefaultTextNodeWidth; - var scale = resNode->ScaleX; - if (scale <= 0f) - scale = 1f; + var scale = resNode->ScaleX; + if (scale <= 0f) + scale = 1f; - var computed = (int)System.Math.Round(rawWidth * scale); - return System.Math.Max(1, computed); -} + 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(); -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) + internal static string ToIconEditorString(string? rawInput) { - pNode->AtkResNode.ToggleVisibility(false); + 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); } } - -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/UiFactory.cs b/LightlessSync/Services/UiFactory.cs index 435d3c2..72681f7 100644 --- a/LightlessSync/Services/UiFactory.cs +++ b/LightlessSync/Services/UiFactory.cs @@ -22,7 +22,6 @@ public class UiFactory private readonly ServerConfigurationManager _serverConfigManager; private readonly LightlessProfileManager _lightlessProfileManager; private readonly PerformanceCollectorService _performanceCollectorService; - private readonly FileDialogManager _fileDialogManager; private readonly ProfileTagService _profileTagService; public UiFactory( @@ -34,7 +33,6 @@ public class UiFactory ServerConfigurationManager serverConfigManager, LightlessProfileManager lightlessProfileManager, PerformanceCollectorService performanceCollectorService, - FileDialogManager fileDialogManager, ProfileTagService profileTagService) { _loggerFactory = loggerFactory; @@ -45,7 +43,6 @@ public class UiFactory _serverConfigManager = serverConfigManager; _lightlessProfileManager = lightlessProfileManager; _performanceCollectorService = performanceCollectorService; - _fileDialogManager = fileDialogManager; _profileTagService = profileTagService; } @@ -59,8 +56,7 @@ public class UiFactory _pairUiService, dto, _performanceCollectorService, - _lightlessProfileManager, - _fileDialogManager); + _lightlessProfileManager); } public StandaloneProfileUi CreateStandaloneProfileUi(Pair pair) diff --git a/LightlessSync/UI/BroadcastUI.cs b/LightlessSync/UI/BroadcastUI.cs index 6878ce3..5540b02 100644 --- a/LightlessSync/UI/BroadcastUI.cs +++ b/LightlessSync/UI/BroadcastUI.cs @@ -2,6 +2,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; +using Dalamud.Utility; using LightlessSync.API.Dto.Group; using LightlessSync.LightlessConfiguration; using LightlessSync.Services; diff --git a/LightlessSync/UI/DrawEntityFactory.cs b/LightlessSync/UI/DrawEntityFactory.cs index ab05f2d..1ecf3f5 100644 --- a/LightlessSync/UI/DrawEntityFactory.cs +++ b/LightlessSync/UI/DrawEntityFactory.cs @@ -21,6 +21,7 @@ namespace LightlessSync.UI; public class DrawEntityFactory { + private readonly ILogger _logger; private readonly ApiController _apiController; private readonly LightlessMediator _mediator; private readonly SelectPairForTagUi _selectPairForTagUi; @@ -32,6 +33,8 @@ public class DrawEntityFactory private readonly SelectTagForPairUi _selectTagForPairUi; private readonly RenamePairTagUi _renamePairTagUi; private readonly SelectTagForSyncshellUi _selectTagForSyncshellUi; + private readonly RenameSyncshellTagUi _renameSyncshellTagUi; + private readonly SelectSyncshellForTagUi _selectSyncshellForTagUi; private readonly TagHandler _tagHandler; private readonly IdDisplayHandler _uidDisplayHandler; private readonly PairLedger _pairLedger; @@ -57,6 +60,7 @@ public class DrawEntityFactory PairLedger pairLedger, PairFactory pairFactory) { + _logger = logger; _apiController = apiController; _uidDisplayHandler = uidDisplayHandler; _selectTagForPairUi = selectTagForPairUi; diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 29e1880..c0f0a68 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -2721,7 +2721,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.Dummy(new Vector2(5)); - _uiShared.ColoredSeparator(UIColors.Get("DimRed"), 3f); + UiSharedService.ColoredSeparator(UIColors.Get("DimRed"), 3f); var onlyUncompressed = textureConfig.OnlyDownscaleUncompressedTextures; if (ImGui.Checkbox("Only downscale uncompressed textures", ref onlyUncompressed)) { @@ -2729,7 +2729,7 @@ public class SettingsUi : WindowMediatorSubscriberBase _playerPerformanceConfigService.Save(); } _uiShared.DrawHelpText("If disabled, compressed textures will be targeted for downscaling too."); - _uiShared.ColoredSeparator(UIColors.Get("DimRed"), 3f); + UiSharedService.ColoredSeparator(UIColors.Get("DimRed"), 3f); ImGui.Dummy(new Vector2(5)); @@ -2737,7 +2737,7 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.Dummy(new Vector2(5)); - _uiShared.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f); ImGui.TreePop(); } diff --git a/LightlessSync/UI/SyncshellAdminUI.cs b/LightlessSync/UI/SyncshellAdminUI.cs index 4941912..27da617 100644 --- a/LightlessSync/UI/SyncshellAdminUI.cs +++ b/LightlessSync/UI/SyncshellAdminUI.cs @@ -1,20 +1,20 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Colors; -using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.Group; +using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; using LightlessSync.Services.Mediator; -using LightlessSync.PlayerData.Pairs; +using LightlessSync.Services.Profiles; +using LightlessSync.UI.Services; using LightlessSync.WebAPI; using Microsoft.Extensions.Logging; using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; using System.Globalization; namespace LightlessSync.UI; @@ -25,38 +25,29 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase private readonly bool _isModerator = false; private readonly bool _isOwner = false; private readonly List _oneTimeInvites = []; - private readonly PairManager _pairManager; private readonly LightlessProfileManager _lightlessProfileManager; - private readonly FileDialogManager _fileDialogManager; private readonly UiSharedService _uiSharedService; + private readonly PairUiService _pairUiService; private List _bannedUsers = []; private LightlessGroupProfileData? _profileData = null; - private bool _adjustedForScollBarsLocalProfile = false; - private bool _adjustedForScollBarsOnlineProfile = false; - private string _descriptionText = string.Empty; private IDalamudTextureWrap? _pfpTextureWrap; private string _profileDescription = string.Empty; - private byte[] _profileImage = []; - private bool _showFileDialogError = false; private int _multiInvites; private string _newPassword; private bool _pwChangeSuccess; private Task? _pruneTestTask; private Task? _pruneTask; private int _pruneDays = 14; - private List _selectedTags = []; public SyncshellAdminUI(ILogger logger, LightlessMediator mediator, ApiController apiController, - UiSharedService uiSharedService, PairManager pairManager, GroupFullInfoDto groupFullInfo, PerformanceCollectorService performanceCollectorService, LightlessProfileManager lightlessProfileManager, FileDialogManager fileDialogManager) + UiSharedService uiSharedService, PairUiService pairUiService, GroupFullInfoDto groupFullInfo, PerformanceCollectorService performanceCollectorService, LightlessProfileManager lightlessProfileManager) : base(logger, mediator, "Syncshell Admin Panel (" + groupFullInfo.GroupAliasOrGID + ")", performanceCollectorService) { GroupFullInfo = groupFullInfo; _apiController = apiController; _uiSharedService = uiSharedService; - _pairManager = pairManager; _lightlessProfileManager = lightlessProfileManager; - _fileDialogManager = fileDialogManager; - + _pairUiService = pairUiService; _isOwner = string.Equals(GroupFullInfo.OwnerUID, _apiController.UID, StringComparison.Ordinal); _isModerator = GroupFullInfo.GroupUserInfo.IsModerator(); _newPassword = string.Empty; @@ -76,6 +67,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase MinimumSize = new(700, 500), MaximumSize = new(700, 2000), }; + _pairUiService = pairUiService; } public GroupFullInfoDto GroupFullInfo { get; private set; } @@ -85,10 +77,13 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase if (!_isModerator && !_isOwner) return; _logger.LogTrace("Drawing Syncshell Admin UI for {group}", GroupFullInfo.GroupAliasOrGID); - GroupFullInfo = _pairManager.Groups[GroupFullInfo.Group]; + var snapshot = _pairUiService.GetSnapshot(); + if (snapshot.GroupsByGid.TryGetValue(GroupFullInfo.Group.GID, out var updatedInfo)) + { + GroupFullInfo = updatedInfo; + } _profileData = _lightlessProfileManager.GetLightlessGroupProfile(GroupFullInfo.Group); - GetTagsFromProfile(); using var id = ImRaii.PushId("syncshell_admin_" + GroupFullInfo.GID); using (_uiSharedService.UidFont.Push()) @@ -207,189 +202,51 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase ownerTab.Dispose(); } } + private void DrawProfile() { var profileTab = ImRaii.TabItem("Profile"); + if (!profileTab) + return; - if (profileTab) + if (_profileData != null) { - if (_uiSharedService.MediumTreeNode("Current Profile", UIColors.Get("LightlessPurple"))) + if (!string.Equals(_profileDescription, _profileData.Description, StringComparison.Ordinal)) { - ImGui.Dummy(new Vector2(5)); - - if (_profileData == null) - { - UiSharedService.ColorTextWrapped("Failed to load profile data.", ImGuiColors.DalamudRed); - ImGui.TreePop(); - return; - } - - if (!_profileImage.SequenceEqual(_profileData.ProfileImageData.Value)) - { - _profileImage = _profileData.ProfileImageData.Value; - _pfpTextureWrap?.Dispose(); - _pfpTextureWrap = _uiSharedService.LoadImage(_profileImage); - } - - if (!string.Equals(_profileDescription, _profileData.Description, StringComparison.OrdinalIgnoreCase)) - { - _profileDescription = _profileData.Description; - _descriptionText = _profileDescription; - } - - if (_pfpTextureWrap != null) - { - ImGui.Image(_pfpTextureWrap.Handle, ImGuiHelpers.ScaledVector2(_pfpTextureWrap.Width, _pfpTextureWrap.Height)); - } - - var spacing = ImGui.GetStyle().ItemSpacing.X; - ImGuiHelpers.ScaledRelativeSameLine(256, spacing); - using (_uiSharedService.GameFont.Push()) - { - var descriptionTextSize = ImGui.CalcTextSize(_profileData.Description, wrapWidth: 256f); - var childFrame = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 256); - if (descriptionTextSize.Y > childFrame.Y) - { - _adjustedForScollBarsOnlineProfile = true; - } - else - { - _adjustedForScollBarsOnlineProfile = false; - } - childFrame = childFrame with - { - X = childFrame.X + (_adjustedForScollBarsOnlineProfile ? ImGui.GetStyle().ScrollbarSize : 0), - }; - if (ImGui.BeginChildFrame(101, childFrame)) - { - UiSharedService.TextWrapped(_profileData.Description); - } - ImGui.EndChildFrame(); - ImGui.TreePop(); - } - var nsfw = _profileData.IsNsfw; - ImGui.BeginDisabled(); - ImGui.Checkbox("Is NSFW", ref nsfw); - ImGui.EndDisabled(); + _profileDescription = _profileData.Description; } - ImGui.Separator(); + UiSharedService.TextWrapped("Preview the Syncshell profile in a standalone window."); - if (_uiSharedService.MediumTreeNode("Profile Settings", UIColors.Get("LightlessPurple"))) + if (_uiSharedService.IconTextButton(FontAwesomeIcon.AddressCard, "Open Syncshell Profile")) { - ImGui.Dummy(new Vector2(5)); - ImGui.TextUnformatted($"Profile Picture:"); - if (_uiSharedService.IconTextButton(FontAwesomeIcon.FileUpload, "Upload new profile picture")) - { - _fileDialogManager.OpenFileDialog("Select new Profile picture", ".png", (success, file) => - { - if (!success) return; - _ = Task.Run(async () => - { - var fileContent = await File.ReadAllBytesAsync(file).ConfigureAwait(false); - MemoryStream ms = new(fileContent); - await using (ms.ConfigureAwait(false)) - { - var format = await Image.DetectFormatAsync(ms).ConfigureAwait(false); - if (!format.FileExtensions.Contains("png", StringComparer.OrdinalIgnoreCase)) - { - _showFileDialogError = true; - return; - } - using var image = Image.Load(fileContent); - - if (image.Width > 512 || image.Height > 512 || (fileContent.Length > 2000 * 1024)) - { - _showFileDialogError = true; - return; - } - - _showFileDialogError = false; - await _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: null, Convert.ToBase64String(fileContent), BannerBase64: null, IsNsfw: null, IsDisabled: null)) - .ConfigureAwait(false); - } - }); - }); - } - UiSharedService.AttachToolTip("Select and upload a new profile picture"); - ImGui.SameLine(); - if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear uploaded profile picture")) - { - _ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: null, PictureBase64: null, BannerBase64: null, IsNsfw: null, IsDisabled: null)); - } - UiSharedService.AttachToolTip("Clear your currently uploaded profile picture"); - if (_showFileDialogError) - { - UiSharedService.ColorTextWrapped("The profile picture must be a PNG file with a maximum height and width of 256px and 250KiB size", ImGuiColors.DalamudRed); - } - ImGui.Separator(); - ImGui.TextUnformatted($"Tags:"); - var childFrameLocal = ImGuiHelpers.ScaledVector2(256 + ImGui.GetStyle().WindowPadding.X + ImGui.GetStyle().WindowBorderSize, 200); - - var allCategoryIndexes = Enum.GetValues() - .Cast() - .ToList(); - - foreach (int tag in allCategoryIndexes) - { - using (ImRaii.PushId($"tag-{tag}")) DrawTag(tag); - } - ImGui.Separator(); - var widthTextBox = 400; - var posX = ImGui.GetCursorPosX(); - ImGui.TextUnformatted($"Description {_descriptionText.Length}/1500"); - ImGui.SetCursorPosX(posX); - ImGuiHelpers.ScaledRelativeSameLine(widthTextBox, ImGui.GetStyle().ItemSpacing.X); - ImGui.TextUnformatted("Preview (approximate)"); - using (_uiSharedService.GameFont.Push()) - ImGui.InputTextMultiline("##description", ref _descriptionText, 1500, ImGuiHelpers.ScaledVector2(widthTextBox, 200)); - - ImGui.SameLine(); - - using (_uiSharedService.GameFont.Push()) - { - var descriptionTextSizeLocal = ImGui.CalcTextSize(_descriptionText, wrapWidth: 256f); - if (descriptionTextSizeLocal.Y > childFrameLocal.Y) - { - _adjustedForScollBarsLocalProfile = true; - } - else - { - _adjustedForScollBarsLocalProfile = false; - } - childFrameLocal = childFrameLocal with - { - X = childFrameLocal.X + (_adjustedForScollBarsLocalProfile ? ImGui.GetStyle().ScrollbarSize : 0), - }; - if (ImGui.BeginChildFrame(102, childFrameLocal)) - { - UiSharedService.TextWrapped(_descriptionText); - } - ImGui.EndChildFrame(); - } - - if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Description")) - { - _ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: _descriptionText, Tags: null, PictureBase64: null, BannerBase64: null, IsNsfw: null, IsDisabled: null)); - } - UiSharedService.AttachToolTip("Sets your profile description text"); - ImGui.SameLine(); - if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear Description")) - { - _ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: null, PictureBase64: null, BannerBase64: null, IsNsfw: null, IsDisabled: null)); - } - UiSharedService.AttachToolTip("Clears your profile description text"); - ImGui.Separator(); - ImGui.TextUnformatted($"Profile Options:"); - var isNsfw = _profileData?.IsNsfw ?? false; - if (ImGui.Checkbox("Profile is NSFW", ref isNsfw)) - { - _ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: null, PictureBase64: null, BannerBase64: null, IsNsfw: isNsfw, IsDisabled: null)); - } - _uiSharedService.DrawHelpText("If your profile description or image can be considered NSFW, toggle this to ON"); - ImGui.TreePop(); + Mediator.Publish(new GroupProfileOpenStandaloneMessage(GroupFullInfo)); } + UiSharedService.AttachToolTip("Opens the standalone Syncshell profile window for this group."); + + ImGuiHelpers.ScaledDummy(2f); + ImGui.TextDisabled("Profile Flags"); + ImGui.BulletText(_profileData.IsNsfw ? "Marked as NSFW" : "Marked as SFW"); + ImGui.BulletText(_profileData.IsDisabled ? "Profile disabled for viewers" : "Profile active"); + + ImGuiHelpers.ScaledDummy(2f); + UiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 1.5f); + ImGuiHelpers.ScaledDummy(2f); + + UiSharedService.TextWrapped("Open the syncshell profile editor to update images, description, tags, and visibility settings."); + ImGuiHelpers.ScaledDummy(2f); + + if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserEdit, "Open Syncshell Profile Editor")) + { + Mediator.Publish(new OpenGroupProfileEditorMessage(GroupFullInfo)); + } + UiSharedService.AttachToolTip("Launches the editor window and associated live preview for this syncshell."); } + else + { + UiSharedService.TextWrapped("Profile information is loading..."); + } + profileTab.Dispose(); } @@ -400,7 +257,8 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase { if (_uiSharedService.MediumTreeNode("User List & Administration", UIColors.Get("LightlessPurple"))) { - if (!_pairManager.GroupPairs.TryGetValue(GroupFullInfo, out var pairs)) + var snapshot = _pairUiService.GetSnapshot(); + if (!snapshot.GroupPairs.TryGetValue(GroupFullInfo, out var pairs)) { UiSharedService.ColorTextWrapped("No users found in this Syncshell", ImGuiColors.DalamudYellow); } @@ -736,33 +594,6 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase } inviteTab.Dispose(); } - private void DrawTag(int tag) - { - var HasTag = _selectedTags.Contains(tag); - var tagName = (ProfileTags)tag; - - if (ImGui.Checkbox(tagName.ToString(), ref HasTag)) - { - if (HasTag) - { - _selectedTags.Add(tag); - _ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: _selectedTags.ToArray(), PictureBase64: null, BannerBase64: null, IsNsfw: null, IsDisabled: null)); - } - else - { - _selectedTags.Remove(tag); - _ = _apiController.GroupSetProfile(new GroupProfileDto(new GroupData(GroupFullInfo.Group.AliasOrGID), Description: null, Tags: _selectedTags.ToArray(), PictureBase64: null, BannerBase64: null, IsNsfw: null, IsDisabled: null)); - } - } - } - - private void GetTagsFromProfile() - { - if (_profileData != null) - { - _selectedTags = [.. _profileData.Tags]; - } - } public override void OnClose() { diff --git a/LightlessSync/UI/SyncshellFinderUI.cs b/LightlessSync/UI/SyncshellFinderUI.cs index 629c18b..0ebfdef 100644 --- a/LightlessSync/UI/SyncshellFinderUI.cs +++ b/LightlessSync/UI/SyncshellFinderUI.cs @@ -474,7 +474,8 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase private async Task RefreshSyncshellsAsync(string? gid = null) { var syncshellBroadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts(); - _currentSyncshells = [.. _pairManager.GroupPairs.Select(g => g.Key)]; + var snapshot = _pairUiService.GetSnapshot(); + _currentSyncshells = [.. snapshot.GroupPairs.Keys]; _recentlyJoined.RemoveWhere(gid => _currentSyncshells.Exists(s => string.Equals(s.GID, gid, StringComparison.Ordinal))); diff --git a/LightlessSync/UI/UISharedService.cs b/LightlessSync/UI/UISharedService.cs index 0673682..b3734a3 100644 --- a/LightlessSync/UI/UISharedService.cs +++ b/LightlessSync/UI/UISharedService.cs @@ -506,7 +506,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase ImGui.Dummy(new Vector2(0, thickness * ImGuiHelpers.GlobalScale)); } - public static void RoundedSeparator(Vector4? color = null, float thickness = 2f, float indent = 0f, float rounding = 4f) + public void RoundedSeparator(Vector4? color = null, float thickness = 2f, float indent = 0f, float rounding = 4f) { float scale = ImGuiHelpers.GlobalScale;