diff --git a/LightlessAPI b/LightlessAPI index f3c6064..4a0b6c3 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit f3c60648921abab03c3a6cc6142543f06ba02c45 +Subproject commit 4a0b6c3e4dccb11b1df5af86d28b99f184455b01 diff --git a/LightlessSync/LightlessConfiguration/Configurations/LightfinderDtrDisplayMode.cs b/LightlessSync/LightlessConfiguration/Configurations/LightfinderDtrDisplayMode.cs new file mode 100644 index 0000000..5c7633c --- /dev/null +++ b/LightlessSync/LightlessConfiguration/Configurations/LightfinderDtrDisplayMode.cs @@ -0,0 +1,7 @@ +namespace LightlessSync.LightlessConfiguration.Configurations; + +public enum LightfinderDtrDisplayMode +{ + NearbyBroadcasts = 0, + PendingPairRequests = 1, +} diff --git a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs index c5e5b2f..3036bdb 100644 --- a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs +++ b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs @@ -28,6 +28,7 @@ public class LightlessConfig : ILightlessConfiguration public DtrEntry.Colors DtrColorsLightfinderDisabled { get; set; } = new(Foreground: 0xD44444u, Glow: 0x642222u); public DtrEntry.Colors DtrColorsLightfinderCooldown { get; set; } = new(Foreground: 0xFFE97Au, Glow: 0x766C3Au); public DtrEntry.Colors DtrColorsLightfinderUnavailable { get; set; } = new(Foreground: 0x000000u, Glow: 0x000000u); + public LightfinderDtrDisplayMode LightfinderDtrDisplayMode { get; set; } = LightfinderDtrDisplayMode.PendingPairRequests; public bool UseLightlessRedesign { get; set; } = true; public bool EnableRightClickMenus { get; set; } = true; public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both; diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index ff9b3d5..9a397be 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -148,9 +148,12 @@ public sealed class Plugin : IDalamudPlugin s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), + s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), - s.GetRequiredService())); + s.GetRequiredService(), + s.GetRequiredService(), + s.GetRequiredService())); collection.AddSingleton(s => new PairManager(s.GetRequiredService>(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), contextMenu, s.GetRequiredService())); collection.AddSingleton(); diff --git a/LightlessSync/Services/BroadcastScanningService.cs b/LightlessSync/Services/BroadcastScanningService.cs index 4619a5e..79fe984 100644 --- a/LightlessSync/Services/BroadcastScanningService.cs +++ b/LightlessSync/Services/BroadcastScanningService.cs @@ -211,6 +211,16 @@ public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDispos UpdateSyncshellBroadcasts(); } + public int CountActiveBroadcasts(string? excludeHashedCid = null) + { + var now = DateTime.UtcNow; + var comparer = StringComparer.Ordinal; + return _broadcastCache.Count(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/Mediator/Messages.cs b/LightlessSync/Services/Mediator/Messages.cs index 963bcbf..dba8427 100644 --- a/LightlessSync/Services/Mediator/Messages.cs +++ b/LightlessSync/Services/Mediator/Messages.cs @@ -17,6 +17,7 @@ namespace LightlessSync.Services.Mediator; public record SwitchToIntroUiMessage : MessageBase; public record SwitchToMainUiMessage : MessageBase; public record OpenSettingsUiMessage : MessageBase; +public record OpenLightfinderSettingsMessage : MessageBase; public record DalamudLoginMessage : MessageBase; public record DalamudLogoutMessage : MessageBase; public record PriorityFrameworkUpdateMessage : SameThreadMessage; diff --git a/LightlessSync/UI/BroadcastUI.cs b/LightlessSync/UI/BroadcastUI.cs index 2e2334a..c760a45 100644 --- a/LightlessSync/UI/BroadcastUI.cs +++ b/LightlessSync/UI/BroadcastUI.cs @@ -1,4 +1,5 @@ using Dalamud.Bindings.ImGui; +using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; using Dalamud.Utility; @@ -252,12 +253,27 @@ namespace LightlessSync.UI _broadcastService.ToggleBroadcast(); } + var toggleButtonHeight = ImGui.GetItemRectSize().Y; + if (isOnCooldown || !_broadcastService.IsLightFinderAvailable) ImGui.EndDisabled(); ImGui.PopStyleColor(); ImGui.PopStyleVar(); + ImGui.SameLine(); + if (_uiSharedService.IconButton(FontAwesomeIcon.Cog, toggleButtonHeight)) + { + Mediator.Publish(new OpenLightfinderSettingsMessage()); + } + + if (ImGui.IsItemHovered()) + { + ImGui.BeginTooltip(); + ImGui.TextUnformatted("Open Lightfinder settings."); + ImGui.EndTooltip(); + } + ImGui.EndTabItem(); } diff --git a/LightlessSync/UI/DtrEntry.cs b/LightlessSync/UI/DtrEntry.cs index 1b3ab61..89c7389 100644 --- a/LightlessSync/UI/DtrEntry.cs +++ b/LightlessSync/UI/DtrEntry.cs @@ -10,6 +10,7 @@ using LightlessSync.Services.Mediator; using LightlessSync.Services.ServerConfiguration; using LightlessSync.WebAPI; using LightlessSync.WebAPI.SignalR.Utils; +using LightlessSync.Utils; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System.Runtime.InteropServices; @@ -19,6 +20,9 @@ namespace LightlessSync.UI; public sealed class DtrEntry : IDisposable, IHostedService { + private static readonly TimeSpan _localHashedCidCacheDuration = TimeSpan.FromMinutes(2); + private static readonly TimeSpan _localHashedCidErrorCooldown = TimeSpan.FromMinutes(1); + private readonly ApiController _apiController; private readonly ServerConfigurationManager _serverManager; private readonly CancellationTokenSource _cancellationTokenSource = new(); @@ -28,8 +32,11 @@ public sealed class DtrEntry : IDisposable, IHostedService private readonly Lazy _lightfinderEntry; private readonly ILogger _logger; private readonly BroadcastService _broadcastService; + private readonly BroadcastScannerService _broadcastScannerService; private readonly LightlessMediator _lightlessMediator; private readonly PairManager _pairManager; + private readonly PairRequestService _pairRequestService; + private readonly DalamudUtilService _dalamudUtilService; private Task? _runTask; private string? _statusText; private string? _statusTooltip; @@ -37,6 +44,10 @@ public sealed class DtrEntry : IDisposable, IHostedService private string? _lightfinderText; private string? _lightfinderTooltip; private Colors _lightfinderColors; + private string? _localHashedCid; + private DateTime _localHashedCidFetchedAt = DateTime.MinValue; + private DateTime _localHashedCidNextErrorLog = DateTime.MinValue; + private DateTime _pairRequestNextErrorLog = DateTime.MinValue; public DtrEntry( ILogger logger, @@ -44,9 +55,12 @@ public sealed class DtrEntry : IDisposable, IHostedService ConfigurationServiceBase configService, LightlessMediator lightlessMediator, PairManager pairManager, + PairRequestService pairRequestService, ApiController apiController, ServerConfigurationManager serverManager, - BroadcastService broadcastService) + BroadcastService broadcastService, + BroadcastScannerService broadcastScannerService, + DalamudUtilService dalamudUtilService) { _logger = logger; _dtrBar = dtrBar; @@ -55,9 +69,12 @@ public sealed class DtrEntry : IDisposable, IHostedService _configService = configService; _lightlessMediator = lightlessMediator; _pairManager = pairManager; + _pairRequestService = pairRequestService; _apiController = apiController; _serverManager = serverManager; _broadcastService = broadcastService; + _broadcastScannerService = broadcastScannerService; + _dalamudUtilService = dalamudUtilService; } public void Dispose() @@ -318,27 +335,99 @@ public sealed class DtrEntry : IDisposable, IHostedService } } + private string? GetLocalHashedCid() + { + var now = DateTime.UtcNow; + if (_localHashedCid is not null && now - _localHashedCidFetchedAt < _localHashedCidCacheDuration) + return _localHashedCid; + + try + { + var cid = _dalamudUtilService.GetCIDAsync().GetAwaiter().GetResult(); + var hashedCid = cid.ToString().GetHash256(); + _localHashedCid = hashedCid; + _localHashedCidFetchedAt = now; + return hashedCid; + } + catch (Exception ex) + { + if (now >= _localHashedCidNextErrorLog) + { + _logger.LogDebug(ex, "Failed to refresh local hashed CID for Lightfinder DTR entry."); + _localHashedCidNextErrorLog = now + _localHashedCidErrorCooldown; + } + + _localHashedCid = null; + _localHashedCidFetchedAt = now; + return null; + } + } + + private int GetNearbyBroadcastCount() + { + var localHashedCid = GetLocalHashedCid(); + return _broadcastScannerService.CountActiveBroadcasts( + string.IsNullOrEmpty(localHashedCid) ? null : localHashedCid); + } + + private int GetPendingPairRequestCount() + { + try + { + return _pairRequestService.GetActiveRequests().Count; + } + 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; + } + } + private (string Text, Colors Colors, string Tooltip) BuildLightfinderIndicator() { var config = _configService.Current; const string icon = "\uE048"; if (!_broadcastService.IsLightFinderAvailable) { - return ($"{icon} --", config.DtrColorsLightfinderUnavailable, "Lightfinder - Unavailable on this server."); + return ($"{icon} --", SwapColorChannels(config.DtrColorsLightfinderUnavailable), "Lightfinder - Unavailable on this server."); } if (_broadcastService.IsBroadcasting) { - return ($"{icon} ON", config.DtrColorsLightfinderEnabled, "Lightfinder - Enabled"); + 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()); + } + default: + { + var broadcastCount = GetNearbyBroadcastCount(); + tooltipBuilder.AppendLine(); + tooltipBuilder.Append("Nearby Lightfinder users: ").Append(broadcastCount); + return ($"{icon} {broadcastCount}", SwapColorChannels(config.DtrColorsLightfinderEnabled), tooltipBuilder.ToString()); + } + } } var tooltip = new StringBuilder("Lightfinder - Disabled"); - var colors = config.DtrColorsLightfinderDisabled; + var colors = SwapColorChannels(config.DtrColorsLightfinderDisabled); if (_broadcastService.RemainingCooldown is { } cooldown && cooldown > TimeSpan.Zero) { tooltip.AppendLine(); tooltip.Append("Cooldown: ").Append(Math.Ceiling(cooldown.TotalSeconds)).Append("s"); - colors = config.DtrColorsLightfinderCooldown; + colors = SwapColorChannels(config.DtrColorsLightfinderCooldown); } return ($"{icon} OFF", colors, tooltip.ToString()); @@ -377,6 +466,17 @@ public sealed class DtrEntry : IDisposable, IHostedService private const byte _colorTypeForeground = 0x13; private const byte _colorTypeGlow = 0x14; + private static Colors SwapColorChannels(Colors colors) + => new(SwapColorComponent(colors.Foreground), SwapColorComponent(colors.Glow)); + + private static uint SwapColorComponent(uint color) + { + if (color == 0) + return 0; + + return ((color & 0xFFu) << 16) | (color & 0xFF00u) | ((color >> 16) & 0xFFu); + } + private static SeString BuildColoredSeString(string text, Colors colors) { var ssb = new SeStringBuilder(); @@ -385,7 +485,16 @@ public sealed class DtrEntry : IDisposable, IHostedService } private static RawPayload BuildColorStartPayload(byte colorType, uint color) - => new(unchecked([0x02, colorType, 0x05, 0xF6, byte.Max((byte)color, 0x01), byte.Max((byte)(color >> 8), 0x01), byte.Max((byte)(color >> 16), 0x01), 0x03])); + => new(unchecked([ + 0x02, + colorType, + 0x05, + 0xF6, + byte.Max((byte)color, (byte)0x01), + byte.Max((byte)(color >> 8), (byte)0x01), + byte.Max((byte)(color >> 16), (byte)0x01), + 0x03 + ])); private static RawPayload BuildColorEndPayload(byte colorType) => new([0x02, colorType, 0x02, 0xEC, 0x03]); diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 52de306..5b2ce6d 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -73,6 +73,8 @@ public class SettingsUi : WindowMediatorSubscriberBase private string _lightfinderIconInput = string.Empty; private bool _lightfinderIconInputInitialized = false; private int _lightfinderIconPresetIndex = -1; + private bool _selectGeneralTabOnNextDraw = false; + private bool _openLightfinderSectionOnNextDraw = false; private static readonly (string Label, SeIconChar Icon)[] LightfinderIconPresets = new[] { ("Link Marker", SeIconChar.LinkMarker), @@ -136,6 +138,12 @@ public class SettingsUi : WindowMediatorSubscriberBase }; Mediator.Subscribe(this, (_) => Toggle()); + Mediator.Subscribe(this, (_) => + { + IsOpen = true; + _selectGeneralTabOnNextDraw = true; + _openLightfinderSectionOnNextDraw = true; + }); Mediator.Subscribe(this, (_) => IsOpen = false); Mediator.Subscribe(this, (_) => UiSharedService_GposeStart()); Mediator.Subscribe(this, (_) => UiSharedService_GposeEnd()); @@ -222,6 +230,17 @@ public class SettingsUi : WindowMediatorSubscriberBase } } + private static DtrEntry.Colors SwapColorChannels(DtrEntry.Colors colors) + => new(SwapColorChannels(colors.Foreground), SwapColorChannels(colors.Glow)); + + private static uint SwapColorChannels(uint color) + { + if (color == 0) + return 0; + + return ((color & 0xFFu) << 16) | (color & 0xFF00u) | ((color >> 16) & 0xFFu); + } + private void DrawBlockedTransfers() { _lastTab = "BlockedTransfers"; @@ -1081,18 +1100,31 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.Separator(); + var forceOpenLightfinder = _openLightfinderSectionOnNextDraw; + if (_openLightfinderSectionOnNextDraw) + { + ImGui.SetNextItemOpen(true, ImGuiCond.Always); + } + if (_uiShared.MediumTreeNode("Lightfinder", UIColors.Get("LightlessPurple"))) { + if (forceOpenLightfinder) + { + ImGui.SetScrollHereY(); + } + + _openLightfinderSectionOnNextDraw = false; + bool autoEnable = _configService.Current.LightfinderAutoEnableOnConnect; var autoAlign = _configService.Current.LightfinderAutoAlign; var offsetX = (int)_configService.Current.LightfinderLabelOffsetX; var offsetY = (int)_configService.Current.LightfinderLabelOffsetY; var labelScale = _configService.Current.LightfinderLabelScale; bool showLightfinderInDtr = _configService.Current.ShowLightfinderInDtr; - var dtrLightfinderEnabled = _configService.Current.DtrColorsLightfinderEnabled; - var dtrLightfinderDisabled = _configService.Current.DtrColorsLightfinderDisabled; - var dtrLightfinderCooldown = _configService.Current.DtrColorsLightfinderCooldown; - var dtrLightfinderUnavailable = _configService.Current.DtrColorsLightfinderUnavailable; + var dtrLightfinderEnabled = SwapColorChannels(_configService.Current.DtrColorsLightfinderEnabled); + var dtrLightfinderDisabled = SwapColorChannels(_configService.Current.DtrColorsLightfinderDisabled); + var dtrLightfinderCooldown = SwapColorChannels(_configService.Current.DtrColorsLightfinderCooldown); + var dtrLightfinderUnavailable = SwapColorChannels(_configService.Current.DtrColorsLightfinderUnavailable); ImGui.TextUnformatted("Connection"); if (ImGui.Checkbox("Auto-enable Lightfinder on server connection", ref autoEnable)) @@ -1112,6 +1144,40 @@ public class SettingsUi : WindowMediatorSubscriberBase } _uiShared.DrawHelpText("Adds a Lightfinder status to the Server info bar. Left click toggles Lightfinder when visible."); + var lightfinderDisplayMode = _configService.Current.LightfinderDtrDisplayMode; + var lightfinderDisplayLabel = lightfinderDisplayMode switch + { + LightfinderDtrDisplayMode.PendingPairRequests => "Pending pair requests", + _ => "Nearby Lightfinder users", + }; + + ImGui.BeginDisabled(!showLightfinderInDtr); + if (ImGui.BeginCombo("Info display", lightfinderDisplayLabel)) + { + foreach (var option in Enum.GetValues()) + { + var optionLabel = option switch + { + LightfinderDtrDisplayMode.PendingPairRequests => "Pending pair requests", + _ => "Nearby Lightfinder users", + }; + + var selected = option == lightfinderDisplayMode; + if (ImGui.Selectable(optionLabel, selected)) + { + _configService.Current.LightfinderDtrDisplayMode = option; + _configService.Save(); + } + + if (selected) + ImGui.SetItemDefaultFocus(); + } + + ImGui.EndCombo(); + } + ImGui.EndDisabled(); + _uiShared.DrawHelpText("Choose what the Lightfinder info bar displays while Lightfinder is active."); + bool useLightfinderColors = _configService.Current.UseLightfinderColorsInDtr; if (ImGui.Checkbox("Color-code the Lightfinder info bar according to status", ref useLightfinderColors)) { @@ -1120,24 +1186,24 @@ public class SettingsUi : WindowMediatorSubscriberBase } ImGui.BeginDisabled(!showLightfinderInDtr || !useLightfinderColors); - if (InputDtrColors("Enables", ref dtrLightfinderEnabled)) + if (InputDtrColors("Enabled", ref dtrLightfinderEnabled)) { - _configService.Current.DtrColorsLightfinderEnabled = dtrLightfinderEnabled; + _configService.Current.DtrColorsLightfinderEnabled = SwapColorChannels(dtrLightfinderEnabled); _configService.Save(); } if (InputDtrColors("Disabled", ref dtrLightfinderDisabled)) { - _configService.Current.DtrColorsLightfinderDisabled = dtrLightfinderDisabled; + _configService.Current.DtrColorsLightfinderDisabled = SwapColorChannels(dtrLightfinderDisabled); _configService.Save(); } if (InputDtrColors("Cooldown", ref dtrLightfinderCooldown)) { - _configService.Current.DtrColorsLightfinderCooldown = dtrLightfinderCooldown; + _configService.Current.DtrColorsLightfinderCooldown = SwapColorChannels(dtrLightfinderCooldown); _configService.Save(); } if (InputDtrColors("Unavailable", ref dtrLightfinderUnavailable)) { - _configService.Current.DtrColorsLightfinderUnavailable = dtrLightfinderUnavailable; + _configService.Current.DtrColorsLightfinderUnavailable = SwapColorChannels(dtrLightfinderUnavailable); _configService.Save(); } ImGui.EndDisabled(); @@ -2696,8 +2762,15 @@ public class SettingsUi : WindowMediatorSubscriberBase ImGui.Separator(); if (ImGui.BeginTabBar("mainTabBar")) { - if (ImGui.BeginTabItem("General")) + var generalTabFlags = ImGuiTabItemFlags.None; + if (_selectGeneralTabOnNextDraw) { + generalTabFlags |= ImGuiTabItemFlags.SetSelected; + } + + if (ImGui.BeginTabItem("General", generalTabFlags)) + { + _selectGeneralTabOnNextDraw = false; DrawGeneral(); ImGui.EndTabItem(); }