diff --git a/LightlessSync/Services/BroadcastScanningService.cs b/LightlessSync/Services/BroadcastScanningService.cs index 79fe984..95abdae 100644 --- a/LightlessSync/Services/BroadcastScanningService.cs +++ b/LightlessSync/Services/BroadcastScanningService.cs @@ -28,7 +28,7 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos private readonly CancellationTokenSource _cleanupCts = new(); private Task? _cleanupTask; - private int _checkEveryFrames = 20; + private readonly int _checkEveryFrames = 20; private int _frameCounter = 0; private int _lookupsThisFrame = 0; private const int MaxLookupsPerFrame = 30; @@ -221,6 +221,16 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos (excludeHashedCid is null || !comparer.Equals(entry.Key, excludeHashedCid))); } + public List> GetActiveBroadcasts(string? excludeHashedCid = null) + { + var now = DateTime.UtcNow; + var comparer = StringComparer.Ordinal; + return [.. _broadcastCache.Where(entry => + entry.Value.IsBroadcasting && + entry.Value.ExpiryTime > now && + (excludeHashedCid is null || !comparer.Equals(entry.Key, excludeHashedCid)))]; + } + protected override void Dispose(bool disposing) { base.Dispose(disposing); diff --git a/LightlessSync/Services/CommandManagerService.cs b/LightlessSync/Services/CommandManagerService.cs index 7aedc7b..88f8780 100644 --- a/LightlessSync/Services/CommandManagerService.cs +++ b/LightlessSync/Services/CommandManagerService.cs @@ -13,7 +13,8 @@ namespace LightlessSync.Services; public sealed class CommandManagerService : IDisposable { - private const string _commandName = "/light"; + private const string _longName = "/lightless"; + private const string _shortName = "/light"; private readonly ApiController _apiController; private readonly ICommandManager _commandManager; @@ -34,7 +35,11 @@ public sealed class CommandManagerService : IDisposable _apiController = apiController; _mediator = mediator; _lightlessConfigService = lightlessConfigService; - _commandManager.AddHandler(_commandName, new CommandInfo(OnCommand) + _commandManager.AddHandler(_longName, new CommandInfo(OnCommand) + { + HelpMessage = $"\u2191;" + }); + _commandManager.AddHandler(_shortName, new CommandInfo(OnCommand) { HelpMessage = "Opens the Lightless Sync UI" + Environment.NewLine + Environment.NewLine + "Additionally possible commands:" + Environment.NewLine + @@ -49,7 +54,8 @@ public sealed class CommandManagerService : IDisposable public void Dispose() { - _commandManager.RemoveHandler(_commandName); + _commandManager.RemoveHandler(_longName); + _commandManager.RemoveHandler(_shortName); } private void OnCommand(string command, string args) diff --git a/LightlessSync/Services/ContextMenuService.cs b/LightlessSync/Services/ContextMenuService.cs index 97bfc17..464fee1 100644 --- a/LightlessSync/Services/ContextMenuService.cs +++ b/LightlessSync/Services/ContextMenuService.cs @@ -98,7 +98,7 @@ internal class ContextMenuService : IHostedService if (targetData == null || targetData.Address == nint.Zero) return; - //Check if user is paired or is own. + //Check if user is directly paired or is own. if (VisibleUserIds.Any(u => u == target.TargetObjectId) || _clientState.LocalPlayer.GameObjectId == target.TargetObjectId) return; @@ -116,7 +116,7 @@ internal class ContextMenuService : IHostedService args.AddMenuItem(new MenuItem { - Name = "Send Pair Request", + Name = "Send Direct Pair Request", PrefixChar = 'L', UseDefaultPrefix = false, PrefixColor = 708, @@ -159,7 +159,7 @@ internal class ContextMenuService : IHostedService } } - private HashSet VisibleUserIds => [.. _pairManager.GetOnlineUserPairs() + private HashSet VisibleUserIds => [.. _pairManager.DirectPairs .Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue) .Select(u => (ulong)u.PlayerCharacterId)]; diff --git a/LightlessSync/Services/NameplateHandler.cs b/LightlessSync/Services/NameplateHandler.cs index a28be5f..11af974 100644 --- a/LightlessSync/Services/NameplateHandler.cs +++ b/LightlessSync/Services/NameplateHandler.cs @@ -1,5 +1,6 @@ using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; +using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.Text; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.System.Framework; @@ -15,8 +16,9 @@ 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; -using System.Text; namespace LightlessSync.Services; @@ -32,10 +34,10 @@ public unsafe class NameplateHandler : IMediatorSubscriber private readonly LightlessMediator _mediator; public LightlessMediator Mediator => _mediator; - private bool mEnabled = false; + private bool _mEnabled = false; private bool _needsLabelRefresh = false; - private AddonNamePlate* mpNameplateAddon = null; - private readonly AtkTextNode*[] mTextNodes = new AtkTextNode*[AddonNamePlate.NumNamePlateObjects]; + private AddonNamePlate* _mpNameplateAddon = null; + private readonly AtkTextNode*[] _mTextNodes = new AtkTextNode*[AddonNamePlate.NumNamePlateObjects]; private readonly int[] _cachedNameplateTextWidths = new int[AddonNamePlate.NumNamePlateObjects]; private readonly int[] _cachedNameplateTextHeights = new int[AddonNamePlate.NumNamePlateObjects]; private readonly int[] _cachedNameplateContainerHeights = new int[AddonNamePlate.NumNamePlateObjects]; @@ -44,10 +46,10 @@ 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 const int _containerOffsetX = 50; private static readonly string DefaultIconGlyph = SeIconCharExtensions.ToIconString(DefaultIcon); - private volatile HashSet _activeBroadcastingCids = []; + private ImmutableHashSet _activeBroadcastingCids = []; public NameplateHandler(ILogger logger, IAddonLifecycle addonLifecycle, IGameGui gameGui, DalamudUtilService dalamudUtil, LightlessConfigService configService, LightlessMediator mediator, IClientState clientState, PairManager pairManager) { @@ -74,17 +76,17 @@ public unsafe class NameplateHandler : IMediatorSubscriber DisableNameplate(); DestroyNameplateNodes(); _mediator.Unsubscribe(this); - mpNameplateAddon = null; + _mpNameplateAddon = null; } internal void EnableNameplate() { - if (!mEnabled) + if (!_mEnabled) { try { _addonLifecycle.RegisterListener(AddonEvent.PostDraw, "NamePlate", NameplateDrawDetour); - mEnabled = true; + _mEnabled = true; } catch (Exception e) { @@ -96,7 +98,7 @@ public unsafe class NameplateHandler : IMediatorSubscriber internal void DisableNameplate() { - if (mEnabled) + if (_mEnabled) { try { @@ -107,24 +109,30 @@ public unsafe class NameplateHandler : IMediatorSubscriber _logger.LogError($"Unknown error while unregistering nameplate listener:\n{e}"); } - mEnabled = false; + _mEnabled = false; HideAllNameplateNodes(); } } private void NameplateDrawDetour(AddonEvent type, AddonArgs args) { + if (args.Addon.Address == nint.Zero) + { + _logger.LogWarning("Nameplate draw detour received a null addon address, skipping update."); + return; + } + var pNameplateAddon = (AddonNamePlate*)args.Addon.Address; - if (mpNameplateAddon != pNameplateAddon) + if (_mpNameplateAddon != pNameplateAddon) { - for (int i = 0; i < mTextNodes.Length; ++i) mTextNodes[i] = null; + 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(); + _mpNameplateAddon = pNameplateAddon; + if (_mpNameplateAddon != null) CreateNameplateNodes(); } UpdateNameplateNodes(); @@ -138,7 +146,16 @@ public unsafe class NameplateHandler : IMediatorSubscriber 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) @@ -148,24 +165,43 @@ public unsafe class NameplateHandler : IMediatorSubscriber pNewNode->AtkResNode.NextSiblingNode = pLastChild; pNewNode->AtkResNode.ParentNode = pNameplateResNode; pLastChild->PrevSiblingNode = (AtkResNode*)pNewNode; - nameplateObject.Value.RootComponentNode->Component->UldManager.UpdateDrawNodeList(); + rootNode->Component->UldManager.UpdateDrawNodeList(); pNewNode->AtkResNode.SetUseDepthBasedPriority(true); - mTextNodes[i] = pNewNode; + _mTextNodes[i] = pNewNode; } } } private void DestroyNameplateNodes() { - var pCurrentNameplateAddon = (AddonNamePlate*)_gameGui.GetAddonByName("NamePlate", 1).Address; - if (mpNameplateAddon == null || mpNameplateAddon != pCurrentNameplateAddon) + 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 pTextNode = _mTextNodes[i]; var pNameplateNode = GetNameplateComponentNode(i); - if (pTextNode != null && pNameplateNode != null) + 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 { @@ -175,7 +211,7 @@ public unsafe class NameplateHandler : IMediatorSubscriber pTextNode->AtkResNode.NextSiblingNode->PrevSiblingNode = pTextNode->AtkResNode.PrevSiblingNode; pNameplateNode->Component->UldManager.UpdateDrawNodeList(); pTextNode->AtkResNode.Destroy(true); - mTextNodes[i] = null; + _mTextNodes[i] = null; } catch (Exception e) { @@ -192,7 +228,7 @@ public unsafe class NameplateHandler : IMediatorSubscriber private void HideAllNameplateNodes() { - for (int i = 0; i < mTextNodes.Length; ++i) + for (int i = 0; i < _mTextNodes.Length; ++i) { HideNameplateTextNode(i); } @@ -200,22 +236,62 @@ public unsafe class NameplateHandler : IMediatorSubscriber private void UpdateNameplateNodes() { - var framework = Framework.Instance(); - var ui3DModule = framework->GetUIModule()->GetUI3DModule(); + var currentHandle = _gameGui.GetAddonByName("NamePlate"); + if (currentHandle.Address == nint.Zero) + { + _logger.LogDebug("NamePlate addon unavailable during update, skipping label refresh."); + return; + } + var currentAddon = (AddonNamePlate*)currentHandle.Address; + if (_mpNameplateAddon == null || currentAddon == null || currentAddon != _mpNameplateAddon) + { + if (_mpNameplateAddon != null) + _logger.LogDebug("Cached NamePlate addon pointer differs from current: waiting for new hook (cached {Cached:X}, current {Current:X}).", (IntPtr)_mpNameplateAddon, (IntPtr)currentAddon); + return; + } + + var framework = Framework.Instance(); + if (framework == null) + { + _logger.LogDebug("Framework instance unavailable during nameplate update, skipping."); + return; + } + + var uiModule = framework->GetUIModule(); + if (uiModule == null) + { + _logger.LogDebug("UI module unavailable during nameplate update, skipping."); + return; + } + + var ui3DModule = uiModule->GetUI3DModule(); if (ui3DModule == null) + { + _logger.LogDebug("UI3D module unavailable during nameplate update, skipping."); + return; + } + + var vec = ui3DModule->NamePlateObjectInfoPointers; + if (vec.IsEmpty) return; - for (int i = 0; i < ui3DModule->NamePlateObjectInfoCount; ++i) + var visibleUserIdsSnapshot = VisibleUserIds; + + var safeCount = System.Math.Min( + ui3DModule->NamePlateObjectInfoCount, + vec.Length + ); + + for (int i = 0; i < safeCount; ++i) { - if (ui3DModule->NamePlateObjectInfoPointers.IsEmpty) continue; + var config = _configService.Current; - var objectInfoPtr = ui3DModule->NamePlateObjectInfoPointers[i]; - - if (objectInfoPtr == null) continue; + var objectInfoPtr = vec[i]; + if (objectInfoPtr == null) + continue; var objectInfo = objectInfoPtr.Value; - if (objectInfo == null || objectInfo->GameObject == null) continue; @@ -223,62 +299,68 @@ public unsafe class NameplateHandler : IMediatorSubscriber if (nameplateIndex < 0 || nameplateIndex >= AddonNamePlate.NumNamePlateObjects) continue; - var pNode = mTextNodes[nameplateIndex]; + var pNode = _mTextNodes[nameplateIndex]; if (pNode == null) continue; - if (mpNameplateAddon == null) + var gameObject = objectInfo->GameObject; + if ((ObjectKind)gameObject->ObjectKind != ObjectKind.Player) + { + pNode->AtkResNode.ToggleVisibility(enable: false); continue; - - var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)objectInfo->GameObject); + } + // CID gating + var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)gameObject); if (cid == null || !_activeBroadcastingCids.Contains(cid)) { - pNode->AtkResNode.ToggleVisibility(false); + pNode->AtkResNode.ToggleVisibility(enable: false); continue; } - if (!_configService.Current.LightfinderLabelShowOwn && (objectInfo->GameObject->GetGameObjectId() == _clientState.LocalPlayer.GameObjectId)) + var local = _clientState.LocalPlayer; + if (!config.LightfinderLabelShowOwn && local != null && + objectInfo->GameObject->GetGameObjectId() == local.GameObjectId) { - pNode->AtkResNode.ToggleVisibility(false); + pNode->AtkResNode.ToggleVisibility(enable: false); continue; } - if (!_configService.Current.LightfinderLabelShowPaired && VisibleUserIds.Any(u => u == objectInfo->GameObject->GetGameObjectId())) + var hidePaired = !config.LightfinderLabelShowPaired; + + var goId = (ulong)gameObject->GetGameObjectId(); + if (hidePaired && visibleUserIdsSnapshot.Contains(goId)) { - pNode->AtkResNode.ToggleVisibility(false); - continue; - } - - var nameplateObject = mpNameplateAddon->NamePlateObjectArray[nameplateIndex]; - nameplateObject.RootComponentNode->Component->UldManager.UpdateDrawNodeList(); - - var pNameplateIconNode = nameplateObject.MarkerIcon; - var pNameplateResNode = nameplateObject.NameContainer; - var pNameplateTextNode = nameplateObject.NameText; - bool IsVisible = pNameplateIconNode->AtkResNode.IsVisible() || (pNameplateResNode->IsVisible() && pNameplateTextNode->AtkResNode.IsVisible()) || _configService.Current.LightfinderLabelShowHidden; - pNode->AtkResNode.ToggleVisibility(IsVisible); - - if (nameplateObject.RootComponentNode == null || - nameplateObject.NameContainer == null || - nameplateObject.NameText == null) - { - pNode->AtkResNode.ToggleVisibility(false); + 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 (nameContainer == null || nameText == null) + if (root == null || root->Component == null || nameContainer == null || nameText == null) { - pNode->AtkResNode.ToggleVisibility(false); + _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 config = _configService.Current; var scaleMultiplier = System.Math.Clamp(config.LightfinderLabelScale, 0.5f, 2.0f); var baseScale = config.LightfinderLabelUseIcon ? 1.0f : 0.5f; @@ -437,7 +519,7 @@ public unsafe class NameplateHandler : IMediatorSubscriber positionY += config.LightfinderLabelOffsetY; alignment = (AlignmentType)System.Math.Clamp((int)alignment, 0, 8); - pNode->AtkResNode.SetUseDepthBasedPriority(true); + pNode->AtkResNode.SetUseDepthBasedPriority(enable: true); pNode->AtkResNode.Color.A = 255; @@ -545,7 +627,7 @@ public unsafe class NameplateHandler : IMediatorSubscriber } private void HideNameplateTextNode(int i) { - var pNode = mTextNodes[i]; + var pNode = _mTextNodes[i]; if (pNode != null) { pNode->AtkResNode.ToggleVisibility(false); @@ -555,10 +637,10 @@ public unsafe class NameplateHandler : IMediatorSubscriber private AddonNamePlate.NamePlateObject? GetNameplateObject(int i) { if (i < AddonNamePlate.NumNamePlateObjects && - mpNameplateAddon != null && - mpNameplateAddon->NamePlateObjectArray[i].RootComponentNode != null) + _mpNameplateAddon != null && + _mpNameplateAddon->NamePlateObjectArray[i].RootComponentNode != null) { - return mpNameplateAddon->NamePlateObjectArray[i]; + return _mpNameplateAddon->NamePlateObjectArray[i]; } else { @@ -571,10 +653,12 @@ public unsafe class NameplateHandler : IMediatorSubscriber 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; @@ -591,18 +675,12 @@ public unsafe class NameplateHandler : IMediatorSubscriber public void UpdateBroadcastingCids(IEnumerable cids) { - var newSet = cids.ToHashSet(); - - var changed = !_activeBroadcastingCids.SetEquals(newSet); - if (!changed) + var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal); + if (ReferenceEquals(_activeBroadcastingCids, newSet) || _activeBroadcastingCids.SetEquals(newSet)) return; - _activeBroadcastingCids.Clear(); - foreach (var cid in newSet) - _activeBroadcastingCids.Add(cid); - - _logger.LogInformation("Active broadcast CIDs: {Cids}", string.Join(",", _activeBroadcastingCids)); - + _activeBroadcastingCids = newSet; + _logger.LogInformation("Active broadcast CIDs: {Cids}", string.Join(',', _activeBroadcastingCids)); FlagRefresh(); } diff --git a/LightlessSync/Services/PairRequestService.cs b/LightlessSync/Services/PairRequestService.cs index 7190825..2531a3a 100644 --- a/LightlessSync/Services/PairRequestService.cs +++ b/LightlessSync/Services/PairRequestService.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using LightlessSync.LightlessConfiguration.Models; using LightlessSync.PlayerData.Pairs; using LightlessSync.Services.Mediator; @@ -14,10 +10,10 @@ public sealed class PairRequestService : DisposableMediatorSubscriberBase private readonly DalamudUtilService _dalamudUtil; private readonly PairManager _pairManager; private readonly Lazy _apiController; - private readonly object _syncRoot = new(); + private readonly Lock _syncRoot = new(); private readonly List _requests = []; - private static readonly TimeSpan Expiration = TimeSpan.FromMinutes(5); + private static readonly TimeSpan _expiration = TimeSpan.FromMinutes(5); public PairRequestService( ILogger logger, @@ -189,7 +185,7 @@ public sealed class PairRequestService : DisposableMediatorSubscriberBase } var now = DateTime.UtcNow; - return _requests.RemoveAll(r => now - r.ReceivedAt > Expiration) > 0; + return _requests.RemoveAll(r => now - r.ReceivedAt > _expiration) > 0; } public void AcceptPairRequest(string hashedCid, string displayName) diff --git a/LightlessSync/UI/CharaDataHubUi.cs b/LightlessSync/UI/CharaDataHubUi.cs index 9016e6c..51723b9 100644 --- a/LightlessSync/UI/CharaDataHubUi.cs +++ b/LightlessSync/UI/CharaDataHubUi.cs @@ -170,7 +170,7 @@ internal sealed partial class CharaDataHubUi : WindowMediatorSubscriberBase if (!_charaDataManager.BrioAvailable) { ImGuiHelpers.ScaledDummy(3); - UiSharedService.DrawGroupedCenteredColorText("To utilize any features related to posing or spawning characters you require to have Brio installed.", ImGuiColors.DalamudRed); + UiSharedService.DrawGroupedCenteredColorText("To utilize any features related to posing or spawning characters, you are required to have Brio installed.", ImGuiColors.DalamudRed); UiSharedService.DistanceSeparator(); } diff --git a/LightlessSync/UI/CompactUI.cs b/LightlessSync/UI/CompactUI.cs index 0700de3..cc8d326 100644 --- a/LightlessSync/UI/CompactUI.cs +++ b/LightlessSync/UI/CompactUI.cs @@ -1,4 +1,3 @@ -using System; using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility; @@ -16,12 +15,14 @@ using LightlessSync.Services.Mediator; using LightlessSync.Services.ServerConfiguration; using LightlessSync.UI.Components; using LightlessSync.UI.Handlers; +using LightlessSync.UI.Models; using LightlessSync.Utils; using LightlessSync.WebAPI; using LightlessSync.WebAPI.Files; using LightlessSync.WebAPI.Files.Models; using LightlessSync.WebAPI.SignalR.Utils; using Microsoft.Extensions.Logging; +using System; using System.Collections.Concurrent; using System.Collections.Immutable; using System.Globalization; @@ -708,23 +709,23 @@ public class CompactUi : WindowMediatorSubscriberBase } //Filter of not foldered syncshells - var groupFolders = new List(); + var groupFolders = new List(); foreach (var group in _pairManager.GroupPairs.Select(g => g.Key).OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase)) { GetGroups(allPairs, filteredPairs, group, out ImmutableList allGroupPairs, out Dictionary> filteredGroupPairs); if (FilterNotTaggedSyncshells(group)) { - groupFolders.Add(_drawEntityFactory.CreateDrawGroupFolder(group, filteredGroupPairs, allGroupPairs)); + groupFolders.Add(new GroupFolder(group, _drawEntityFactory.CreateDrawGroupFolder(group, filteredGroupPairs, allGroupPairs))); } } //Filter of grouped up syncshells (All Syncshells Folder) if (_configService.Current.GroupUpSyncshells) - drawFolders.Add(new DrawGroupedGroupFolder(groupFolders, _tagHandler, _uiSharedService, + drawFolders.Add(new DrawGroupedGroupFolder(groupFolders, _tagHandler, _apiController, _uiSharedService, _selectSyncshellForTagUi, _renameSyncshellTagUi, "")); else - drawFolders.AddRange(groupFolders); + drawFolders.AddRange(groupFolders.Select(v => v.GroupDrawFolder)); //Filter of grouped/foldered pairs foreach (var tag in _tagHandler.GetAllPairTagsSorted()) @@ -738,7 +739,7 @@ public class CompactUi : WindowMediatorSubscriberBase //Filter of grouped/foldered syncshells foreach (var syncshellTag in _tagHandler.GetAllSyncshellTagsSorted()) { - var syncshellFolderTags = new List(); + var syncshellFolderTags = new List(); foreach (var group in _pairManager.GroupPairs.Select(g => g.Key).OrderBy(g => g.GroupAliasOrGID, StringComparer.OrdinalIgnoreCase)) { if (_tagHandler.HasSyncshellTag(group.GID, syncshellTag)) @@ -747,11 +748,11 @@ public class CompactUi : WindowMediatorSubscriberBase out ImmutableList allGroupPairs, out Dictionary> filteredGroupPairs); - syncshellFolderTags.Add(_drawEntityFactory.CreateDrawGroupFolder($"tag_{group.GID}", group, filteredGroupPairs, allGroupPairs)); + syncshellFolderTags.Add(new GroupFolder(group, _drawEntityFactory.CreateDrawGroupFolder($"tag_{group.GID}", group, filteredGroupPairs, allGroupPairs))); } } - drawFolders.Add(new DrawGroupedGroupFolder(syncshellFolderTags, _tagHandler, _uiSharedService, _selectSyncshellForTagUi, _renameSyncshellTagUi, syncshellTag)); + drawFolders.Add(new DrawGroupedGroupFolder(syncshellFolderTags, _tagHandler, _apiController, _uiSharedService, _selectSyncshellForTagUi, _renameSyncshellTagUi, syncshellTag)); } //Filter of not grouped/foldered and offline pairs diff --git a/LightlessSync/UI/Components/DrawGroupedGroupFolder.cs b/LightlessSync/UI/Components/DrawGroupedGroupFolder.cs index 2aa3d5c..1bb3d79 100644 --- a/LightlessSync/UI/Components/DrawGroupedGroupFolder.cs +++ b/LightlessSync/UI/Components/DrawGroupedGroupFolder.cs @@ -1,7 +1,11 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; +using LightlessSync.API.Data.Extensions; +using LightlessSync.API.Dto.Group; using LightlessSync.UI.Handlers; +using LightlessSync.UI.Models; +using LightlessSync.WebAPI; using System.Collections.Immutable; using System.Numerics; @@ -10,19 +14,20 @@ namespace LightlessSync.UI.Components; public class DrawGroupedGroupFolder : IDrawFolder { private readonly string _tag; - private readonly IEnumerable _groups; + private readonly IEnumerable _groups; private readonly TagHandler _tagHandler; private readonly UiSharedService _uiSharedService; + private readonly ApiController _apiController; private readonly SelectSyncshellForTagUi _selectSyncshellForTagUi; private readonly RenameSyncshellTagUi _renameSyncshellTagUi; private bool _wasHovered = false; private float _menuWidth; - public IImmutableList DrawPairs => throw new NotSupportedException(); - public int OnlinePairs => _groups.SelectMany(g => g.DrawPairs).Where(g => g.Pair.IsOnline).DistinctBy(g => g.Pair.UserData.UID).Count(); - public int TotalPairs => _groups.Sum(g => g.TotalPairs); + public IImmutableList DrawPairs => _groups.SelectMany(g => g.GroupDrawFolder.DrawPairs).ToImmutableList(); + public int OnlinePairs => _groups.SelectMany(g => g.GroupDrawFolder.DrawPairs).Where(g => g.Pair.IsOnline).DistinctBy(g => g.Pair.UserData.UID).Count(); + public int TotalPairs => _groups.Sum(g => g.GroupDrawFolder.TotalPairs); - public DrawGroupedGroupFolder(IEnumerable groups, TagHandler tagHandler, UiSharedService uiSharedService, SelectSyncshellForTagUi selectSyncshellForTagUi, RenameSyncshellTagUi renameSyncshellTagUi, string tag) + public DrawGroupedGroupFolder(IEnumerable groups, TagHandler tagHandler, ApiController apiController, UiSharedService uiSharedService, SelectSyncshellForTagUi selectSyncshellForTagUi, RenameSyncshellTagUi renameSyncshellTagUi, string tag) { _groups = groups; _tagHandler = tagHandler; @@ -30,6 +35,7 @@ public class DrawGroupedGroupFolder : IDrawFolder _selectSyncshellForTagUi = selectSyncshellForTagUi; _renameSyncshellTagUi = renameSyncshellTagUi; _tag = tag; + _apiController = apiController; } public void Draw() @@ -42,7 +48,7 @@ public class DrawGroupedGroupFolder : IDrawFolder using var id = ImRaii.PushId(_id); var color = ImRaii.PushColor(ImGuiCol.ChildBg, ImGui.GetColorU32(ImGuiCol.FrameBgHovered), _wasHovered); - using (ImRaii.Child("folder__" + _id, new System.Numerics.Vector2(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetCursorPosX(), ImGui.GetFrameHeight()))) + using (ImRaii.Child("folder__" + _id, new Vector2(UiSharedService.GetWindowContentRegionWidth() - ImGui.GetCursorPosX(), ImGui.GetFrameHeight()))) { ImGui.Dummy(new Vector2(0f, ImGui.GetFrameHeight())); using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(0f, 0f))) @@ -83,11 +89,16 @@ public class DrawGroupedGroupFolder : IDrawFolder { ImGui.TextUnformatted(_tag); + ImGui.SameLine(); + DrawPauseButton(); ImGui.SameLine(); DrawMenu(); } else { ImGui.TextUnformatted("All Syncshells"); + + ImGui.SameLine(); + DrawPauseButton(); } } color.Dispose(); @@ -100,11 +111,49 @@ public class DrawGroupedGroupFolder : IDrawFolder using var indent = ImRaii.PushIndent(20f); foreach (var entry in _groups) { - entry.Draw(); + entry.GroupDrawFolder.Draw(); } } } + protected void DrawPauseButton() + { + if (DrawPairs.Count > 0) + { + var isPaused = _groups.Select(g => g.GroupFullInfo).All(g => g.GroupUserPermissions.IsPaused()); + FontAwesomeIcon pauseIcon = isPaused ? FontAwesomeIcon.Play : FontAwesomeIcon.Pause; + + var pauseButtonSize = _uiSharedService.GetIconButtonSize(pauseIcon); + var windowEndX = ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth(); + if (_tag != "") + { + var spacingX = ImGui.GetStyle().ItemSpacing.X; + var menuButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.EllipsisV); + ImGui.SameLine(windowEndX - pauseButtonSize.X - menuButtonSize.X - spacingX); + } + else + { + ImGui.SameLine(windowEndX - pauseButtonSize.X); + } + + + if (_uiSharedService.IconButton(pauseIcon)) + { + ChangePauseStateGroups(); + } + } + } + + protected void ChangePauseStateGroups() + { + foreach(var group in _groups) + { + var perm = group.GroupFullInfo.GroupUserPermissions; + perm.SetPaused(!perm.IsPaused()); + _ = _apiController.GroupChangeIndividualPermissionState(new GroupPairUserPermissionDto(group.GroupFullInfo.Group, new(_apiController.UID), perm)); + } + } + protected void DrawMenu() { var barButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.EllipsisV); diff --git a/LightlessSync/UI/Components/IDrawFolder.cs b/LightlessSync/UI/Components/IDrawFolder.cs index eda1fce..faf8a69 100644 --- a/LightlessSync/UI/Components/IDrawFolder.cs +++ b/LightlessSync/UI/Components/IDrawFolder.cs @@ -1,5 +1,4 @@ - -using System.Collections.Immutable; +using System.Collections.Immutable; namespace LightlessSync.UI.Components; diff --git a/LightlessSync/UI/DtrEntry.cs b/LightlessSync/UI/DtrEntry.cs index 89c7389..17bc871 100644 --- a/LightlessSync/UI/DtrEntry.cs +++ b/LightlessSync/UI/DtrEntry.cs @@ -2,19 +2,22 @@ using Dalamud.Game.Gui.Dtr; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Plugin.Services; +using Dalamud.Utility; using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration.Configurations; using LightlessSync.PlayerData.Pairs; using LightlessSync.Services; using LightlessSync.Services.Mediator; using LightlessSync.Services.ServerConfiguration; +using LightlessSync.Utils; using LightlessSync.WebAPI; using LightlessSync.WebAPI.SignalR.Utils; -using LightlessSync.Utils; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; using System.Runtime.InteropServices; using System.Text; +using static LightlessSync.Services.PairRequestService; namespace LightlessSync.UI; @@ -106,7 +109,7 @@ public sealed class DtrEntry : IDisposable, IHostedService } catch (OperationCanceledException) { - + _logger.LogInformation("Lightfinder operation was canceled."); } finally { @@ -363,29 +366,46 @@ public sealed class DtrEntry : IDisposable, IHostedService } } - private int GetNearbyBroadcastCount() - { - var localHashedCid = GetLocalHashedCid(); - return _broadcastScannerService.CountActiveBroadcasts( - string.IsNullOrEmpty(localHashedCid) ? null : localHashedCid); - } - - private int GetPendingPairRequestCount() + private List GetNearbyBroadcasts() { try { - return _pairRequestService.GetActiveRequests().Count; + var localHashedCid = GetLocalHashedCid(); + return [.. _broadcastScannerService + .GetActiveBroadcasts(string.IsNullOrEmpty(localHashedCid) ? null : localHashedCid) + .Select(b => _dalamudUtilService.FindPlayerByNameHash(b.Key).Name)]; } catch (Exception ex) { var now = DateTime.UtcNow; + + if (now >= _pairRequestNextErrorLog) + { + _logger.LogDebug(ex, "Failed to retrieve nearby broadcasts for Lightfinder DTR entry."); + _pairRequestNextErrorLog = now + _localHashedCidErrorCooldown; + } + + return []; + } + } + + private IReadOnlyList GetPendingPairRequest() + { + try + { + return _pairRequestService.GetActiveRequests(); + } + catch (Exception ex) + { + var now = DateTime.UtcNow; + if (now >= _pairRequestNextErrorLog) { _logger.LogDebug(ex, "Failed to retrieve pair request count for Lightfinder DTR entry."); _pairRequestNextErrorLog = now + _localHashedCidErrorCooldown; } - return 0; + return []; } } @@ -400,23 +420,15 @@ public sealed class DtrEntry : IDisposable, IHostedService if (_broadcastService.IsBroadcasting) { - var tooltipBuilder = new StringBuilder("Lightfinder - Enabled"); - switch (config.LightfinderDtrDisplayMode) { case LightfinderDtrDisplayMode.PendingPairRequests: { - var requestCount = GetPendingPairRequestCount(); - tooltipBuilder.AppendLine(); - tooltipBuilder.Append("Pending pair requests: ").Append(requestCount); - return ($"{icon} Requests {requestCount}", SwapColorChannels(config.DtrColorsLightfinderEnabled), tooltipBuilder.ToString()); + return FormatTooltip("Pending pair requests", GetPendingPairRequest().Select(x => x.DisplayName), icon, SwapColorChannels(config.DtrColorsLightfinderEnabled)); } default: { - var broadcastCount = GetNearbyBroadcastCount(); - tooltipBuilder.AppendLine(); - tooltipBuilder.Append("Nearby Lightfinder users: ").Append(broadcastCount); - return ($"{icon} {broadcastCount}", SwapColorChannels(config.DtrColorsLightfinderEnabled), tooltipBuilder.ToString()); + return FormatTooltip("Nearby Lightfinder users", GetNearbyBroadcasts(), icon, SwapColorChannels(config.DtrColorsLightfinderEnabled)); } } } @@ -433,6 +445,18 @@ public sealed class DtrEntry : IDisposable, IHostedService return ($"{icon} OFF", colors, tooltip.ToString()); } + private (string, Colors, string) FormatTooltip(string title, IEnumerable names, string icon, Colors color) + { + var list = names.Where(x => !string.IsNullOrEmpty(x)).ToList(); + var tooltip = new StringBuilder() + .Append($"Lightfinder - Enabled{Environment.NewLine}") + .Append($"{title}: {list.Count}{Environment.NewLine}") + .AppendJoin(Environment.NewLine, list) + .ToString(); + + return ($"{icon} {list.Count}", color, tooltip); + } + private static string BuildLightfinderTooltip(string baseTooltip) { var builder = new StringBuilder(); diff --git a/LightlessSync/UI/Models/GroupFolder.cs b/LightlessSync/UI/Models/GroupFolder.cs new file mode 100644 index 0000000..cedeef0 --- /dev/null +++ b/LightlessSync/UI/Models/GroupFolder.cs @@ -0,0 +1,6 @@ +using LightlessSync.API.Dto.Group; +using LightlessSync.UI.Components; + +namespace LightlessSync.UI.Models; + +public record GroupFolder(GroupFullInfoDto GroupFullInfo, IDrawFolder GroupDrawFolder);