diff --git a/LightlessAPI b/LightlessAPI index 6c542c0..f3c6064 160000 --- a/LightlessAPI +++ b/LightlessAPI @@ -1 +1 @@ -Subproject commit 6c542c0ccca0327896ef895f9de02a76869ea311 +Subproject commit f3c60648921abab03c3a6cc6142543f06ba02c45 diff --git a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs index c20d631..c5e5b2f 100644 --- a/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs +++ b/LightlessSync/LightlessConfiguration/Configurations/LightlessConfig.cs @@ -22,6 +22,12 @@ public class LightlessConfig : ILightlessConfiguration public DtrEntry.Colors DtrColorsDefault { get; set; } = default; public DtrEntry.Colors DtrColorsNotConnected { get; set; } = new(Glow: 0x0428FFu); public DtrEntry.Colors DtrColorsPairsInRange { get; set; } = new(Glow: 0xFFBA47u); + public bool ShowLightfinderInDtr { get; set; } = false; + public bool UseLightfinderColorsInDtr { get; set; } = true; + public DtrEntry.Colors DtrColorsLightfinderEnabled { get; set; } = new(Foreground: 0xB590FFu, Glow: 0x4F406Eu); + 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 bool UseLightlessRedesign { get; set; } = true; public bool EnableRightClickMenus { get; set; } = true; public NotificationLocation ErrorNotification { get; set; } = NotificationLocation.Both; @@ -75,6 +81,7 @@ public class LightlessConfig : ILightlessConfiguration public bool overrideFcTagColor { get; set; } = false; public bool useColoredUIDs { get; set; } = true; public bool BroadcastEnabled { get; set; } = false; + public bool LightfinderAutoEnableOnConnect { get; set; } = false; public short LightfinderLabelOffsetX { get; set; } = 0; public short LightfinderLabelOffsetY { get; set; } = 0; public bool LightfinderLabelUseIcon { get; set; } = false; diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index c1648ca..ff9b3d5 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -142,8 +142,15 @@ public sealed class Plugin : IDalamudPlugin clientState, objectTable, framework, gameGui, condition, gameData, targetManager, gameConfig, s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); - collection.AddSingleton((s) => new DtrEntry(s.GetRequiredService>(), dtrBar, s.GetRequiredService(), - s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService(), s.GetRequiredService())); + collection.AddSingleton((s) => new DtrEntry( + s.GetRequiredService>(), + dtrBar, + 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(); @@ -279,4 +286,4 @@ public sealed class Plugin : IDalamudPlugin _host.StopAsync().GetAwaiter().GetResult(); _host.Dispose(); } -} \ No newline at end of file +} diff --git a/LightlessSync/Services/BroadcastService.cs b/LightlessSync/Services/BroadcastService.cs index bf9eb05..0cdd3c0 100644 --- a/LightlessSync/Services/BroadcastService.cs +++ b/LightlessSync/Services/BroadcastService.cs @@ -7,6 +7,7 @@ using LightlessSync.WebAPI; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using System.Threading; namespace LightlessSync.Services; public class BroadcastService : IHostedService, IMediatorSubscriber @@ -16,9 +17,11 @@ public class BroadcastService : IHostedService, IMediatorSubscriber private readonly LightlessMediator _mediator; private readonly LightlessConfigService _config; private readonly DalamudUtilService _dalamudUtil; + private CancellationTokenSource? _lightfinderCancelTokens; + private Action? _connectedHandler; public LightlessMediator Mediator => _mediator; - public bool IsLightFinderAvailable { get; private set; } = true; + public bool IsLightFinderAvailable { get; private set; } = false; public bool IsBroadcasting => _config.Current.BroadcastEnabled; private bool _syncedOnStartup = false; @@ -57,24 +60,125 @@ public class BroadcastService : IHostedService, IMediatorSubscriber await action().ConfigureAwait(false); } - public async Task StartAsync(CancellationToken cancellationToken) + private async Task GetLocalHashedCidAsync(string context) + { + try + { + var cid = await _dalamudUtil.GetCIDAsync().ConfigureAwait(false); + return cid.ToString().GetHash256(); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to resolve CID for {Context}", context); + return null; + } + } + + private void ApplyBroadcastDisabled(bool forcePublish = false) + { + bool wasEnabled = _config.Current.BroadcastEnabled; + bool hadExpiry = _config.Current.BroadcastTtl != DateTime.MinValue; + bool hadRemaining = _remainingTtl.HasValue; + + _config.Current.BroadcastEnabled = false; + _config.Current.BroadcastTtl = DateTime.MinValue; + + if (wasEnabled || hadExpiry) + _config.Save(); + + _remainingTtl = null; + _waitingForTtlFetch = false; + _syncedOnStartup = false; + + if (forcePublish || wasEnabled || hadRemaining) + _mediator.Publish(new BroadcastStatusChangedMessage(false, null)); + } + + private bool TryApplyBroadcastEnabled(TimeSpan? ttl, string context) + { + if (ttl is not { } validTtl || validTtl <= TimeSpan.Zero) + { + _logger.LogWarning("Lightfinder enable skipped ({Context}): invalid TTL ({TTL})", context, ttl); + return false; + } + + bool wasEnabled = _config.Current.BroadcastEnabled; + TimeSpan? previousRemaining = _remainingTtl; + DateTime previousExpiry = _config.Current.BroadcastTtl; + + var newExpiry = DateTime.UtcNow + validTtl; + + _config.Current.BroadcastEnabled = true; + _config.Current.BroadcastTtl = newExpiry; + + if (!wasEnabled || previousExpiry != newExpiry) + _config.Save(); + + _remainingTtl = validTtl; + _waitingForTtlFetch = false; + + if (!wasEnabled || previousRemaining != validTtl) + _mediator.Publish(new BroadcastStatusChangedMessage(true, validTtl)); + + _logger.LogInformation("Lightfinder broadcast enabled ({Context}), TTL: {TTL}", context, validTtl); + return true; + } + + private void HandleLightfinderUnavailable(string message, Exception? ex = null) + { + if (ex != null) + _logger.LogWarning(ex, message); + else + _logger.LogWarning(message); + + IsLightFinderAvailable = false; + ApplyBroadcastDisabled(forcePublish: true); + } + + private void OnDisconnected() + { + IsLightFinderAvailable = false; + ApplyBroadcastDisabled(forcePublish: true); + _logger.LogDebug("Cleared Lightfinder state due to disconnect."); + } + + public Task StartAsync(CancellationToken cancellationToken) { _mediator.Subscribe(this, OnEnableBroadcast); _mediator.Subscribe(this, OnBroadcastStatusChanged); _mediator.Subscribe(this, OnTick); + _mediator.Subscribe(this, _ => OnDisconnected()); - _apiController.OnConnected += () => _ = CheckLightfinderSupportAsync(cancellationToken); - //_ = CheckLightfinderSupportAsync(cancellationToken); + IsLightFinderAvailable = false; + + _lightfinderCancelTokens?.Cancel(); + _lightfinderCancelTokens?.Dispose(); + _lightfinderCancelTokens = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + _connectedHandler = () => _ = CheckLightfinderSupportAsync(_lightfinderCancelTokens.Token); + _apiController.OnConnected += _connectedHandler; + + if (_apiController.IsConnected) + _ = CheckLightfinderSupportAsync(_lightfinderCancelTokens.Token); + + return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) { + _lightfinderCancelTokens?.Cancel(); + _lightfinderCancelTokens?.Dispose(); + _lightfinderCancelTokens = null; + + if (_connectedHandler is not null) + { + _apiController.OnConnected -= _connectedHandler; + _connectedHandler = null; + } + _mediator.UnsubscribeAll(this); - _apiController.OnConnected -= () => _ = CheckLightfinderSupportAsync(cancellationToken); return Task.CompletedTask; } - // need to rework this, this is cooked private async Task CheckLightfinderSupportAsync(CancellationToken cancellationToken) { try @@ -85,25 +189,54 @@ public class BroadcastService : IHostedService, IMediatorSubscriber if (cancellationToken.IsCancellationRequested) return; - var dummy = "0".PadLeft(64, '0'); + var hashedCid = await GetLocalHashedCidAsync("Lightfinder state check").ConfigureAwait(false); + if (string.IsNullOrEmpty(hashedCid)) + return; - await _apiController.IsUserBroadcasting(dummy).ConfigureAwait(false); - await _apiController.SetBroadcastStatus(dummy, true, null).ConfigureAwait(false); - await _apiController.GetBroadcastTtl(dummy).ConfigureAwait(false); - await _apiController.AreUsersBroadcasting([dummy]).ConfigureAwait(false); + BroadcastStatusInfoDto? status = null; + try + { + status = await _apiController.IsUserBroadcasting(hashedCid).ConfigureAwait(false); + } + catch (HubException ex) when (ex.Message.Contains("Method does not exist", StringComparison.OrdinalIgnoreCase)) + { + HandleLightfinderUnavailable("Lightfinder unavailable on server (required method missing).", ex); + } + + if (!IsLightFinderAvailable) + _logger.LogInformation("Lightfinder is available."); IsLightFinderAvailable = true; - _logger.LogInformation("Lightfinder is available."); - } - catch (HubException ex) when (ex.Message.Contains("Method does not exist")) - { - _logger.LogWarning("Lightfinder unavailable: required method missing."); - IsLightFinderAvailable = false; - _config.Current.BroadcastEnabled = false; - _config.Current.BroadcastTtl = DateTime.MinValue; - _config.Save(); - _mediator.Publish(new BroadcastStatusChangedMessage(false, null)); + bool isBroadcasting = status?.IsBroadcasting == true; + TimeSpan? ttl = status?.TTL; + + if (isBroadcasting) + { + if (ttl is not { } remaining || remaining <= TimeSpan.Zero) + ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false); + + if (TryApplyBroadcastEnabled(ttl, "server handshake")) + { + _syncedOnStartup = true; + } + else + { + isBroadcasting = false; + } + } + + if (!isBroadcasting) + { + ApplyBroadcastDisabled(forcePublish: true); + _logger.LogInformation("Lightfinder is available but no active broadcast was found."); + } + + if (_config.Current.LightfinderAutoEnableOnConnect && !isBroadcasting) + { + _logger.LogInformation("Auto-enabling Lightfinder broadcast after reconnect."); + _mediator.Publish(new EnableBroadcastMessage(hashedCid, true)); + } } catch (OperationCanceledException) { @@ -111,14 +244,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber } catch (Exception ex) { - _logger.LogWarning(ex, "Lightfinder check failed."); - IsLightFinderAvailable = false; - - _config.Current.BroadcastEnabled = false; - _config.Current.BroadcastTtl = DateTime.MinValue; - _config.Save(); - - _mediator.Publish(new BroadcastStatusChangedMessage(false, null)); + HandleLightfinderUnavailable("Lightfinder check failed.", ex); } } @@ -139,46 +265,38 @@ public class BroadcastService : IHostedService, IMediatorSubscriber }; } - await _apiController.SetBroadcastStatus(msg.HashedCid, msg.Enabled, groupDto).ConfigureAwait(false); + await _apiController.SetBroadcastStatus(msg.Enabled, groupDto).ConfigureAwait(false); _logger.LogDebug("Broadcast {Status} for {Cid}", msg.Enabled ? "enabled" : "disabled", msg.HashedCid); if (!msg.Enabled) { - _config.Current.BroadcastEnabled = false; - _config.Current.BroadcastTtl = DateTime.MinValue; - _config.Save(); - - _mediator.Publish(new BroadcastStatusChangedMessage(false, null)); + ApplyBroadcastDisabled(forcePublish: true); Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Disabled Lightfinder for Player: {msg.HashedCid}"))); return; } _waitingForTtlFetch = true; - TimeSpan? ttl = await GetBroadcastTtlAsync(msg.HashedCid).ConfigureAwait(false); - - if (ttl is { } remaining && remaining > TimeSpan.Zero) + try { - _config.Current.BroadcastTtl = DateTime.UtcNow + remaining; - _config.Current.BroadcastEnabled = true; - _config.Save(); + TimeSpan? ttl = await GetBroadcastTtlAsync(msg.HashedCid).ConfigureAwait(false); - _logger.LogDebug("Fetched TTL from server: {TTL}", remaining); - _mediator.Publish(new BroadcastStatusChangedMessage(true, remaining)); - Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Enabled Lightfinder for Player: {msg.HashedCid}"))); + if (TryApplyBroadcastEnabled(ttl, "client request")) + { + _logger.LogDebug("Fetched TTL from server: {TTL}", ttl); + Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational, $"Enabled Lightfinder for Player: {msg.HashedCid}"))); + } + else + { + ApplyBroadcastDisabled(forcePublish: true); + _logger.LogWarning("No valid TTL returned after enabling broadcast. Disabling."); + } } - else + finally { - _logger.LogWarning("No valid TTL returned after enabling broadcast. Disabling."); - _config.Current.BroadcastEnabled = false; - _config.Current.BroadcastTtl = DateTime.MinValue; - _config.Save(); - - _mediator.Publish(new BroadcastStatusChangedMessage(false, null)); + _waitingForTtlFetch = false; } - - _waitingForTtlFetch = false; } catch (Exception ex) { @@ -219,17 +337,24 @@ public class BroadcastService : IHostedService, IMediatorSubscriber return result; } - public async Task GetBroadcastTtlAsync(string cid) + public async Task GetBroadcastTtlAsync(string? cidForLog = null) { TimeSpan? ttl = null; await RequireConnectionAsync(nameof(GetBroadcastTtlAsync), async () => { try { - ttl = await _apiController.GetBroadcastTtl(cid).ConfigureAwait(false); + ttl = await _apiController.GetBroadcastTtl().ConfigureAwait(false); } catch (Exception ex) { - _logger.LogWarning(ex, "Failed to fetch broadcast TTL for {cid}", cid); + if (cidForLog is { Length: > 0 }) + { + _logger.LogWarning(ex, "Failed to fetch broadcast TTL for {Cid}", cidForLog); + } + else + { + _logger.LogWarning(ex, "Failed to fetch broadcast TTL"); + } } }).ConfigureAwait(false); return ttl; @@ -281,7 +406,12 @@ public class BroadcastService : IHostedService, IMediatorSubscriber return; } - var hashedCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256(); + var hashedCid = await GetLocalHashedCidAsync(nameof(ToggleBroadcast)).ConfigureAwait(false); + if (string.IsNullOrEmpty(hashedCid)) + { + _logger.LogWarning("ToggleBroadcast - unable to resolve CID."); + return; + } try { @@ -321,31 +451,31 @@ public class BroadcastService : IHostedService, IMediatorSubscriber await RequireConnectionAsync(nameof(OnTick), async () => { if (!_syncedOnStartup && _config.Current.BroadcastEnabled) { - _syncedOnStartup = true; try { - string hashedCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256(); - TimeSpan? ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false); - if (ttl is { } - remaining && remaining > TimeSpan.Zero) + var hashedCid = await GetLocalHashedCidAsync("startup TTL refresh").ConfigureAwait(false); + if (string.IsNullOrEmpty(hashedCid)) { - _config.Current.BroadcastTtl = DateTime.UtcNow + remaining; - _config.Current.BroadcastEnabled = true; - _config.Save(); - _logger.LogDebug("Refreshed broadcast TTL from server on first OnTick: {TTL}", remaining); + _logger.LogDebug("Skipping TTL refresh; hashed CID unavailable."); + return; + } + + TimeSpan? ttl = await GetBroadcastTtlAsync(hashedCid).ConfigureAwait(false); + if (TryApplyBroadcastEnabled(ttl, "startup TTL refresh")) + { + _syncedOnStartup = true; + _logger.LogDebug("Refreshed broadcast TTL from server on first OnTick: {TTL}", ttl); } else { _logger.LogWarning("No valid TTL found on OnTick. Disabling broadcast state."); - _config.Current.BroadcastEnabled = false; - _config.Current.BroadcastTtl = DateTime.MinValue; - _config.Save(); - _mediator.Publish(new BroadcastStatusChangedMessage(false, null)); + ApplyBroadcastDisabled(forcePublish: true); } } catch (Exception ex) { _logger.LogError(ex, "Failed to refresh TTL in OnTick"); + _syncedOnStartup = false; } } if (_config.Current.BroadcastEnabled) @@ -362,10 +492,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber if (_remainingTtl == null) { _logger.LogDebug("Broadcast TTL expired. Disabling broadcast locally."); - _config.Current.BroadcastEnabled = false; - _config.Current.BroadcastTtl = DateTime.MinValue; - _config.Save(); - _mediator.Publish(new BroadcastStatusChangedMessage(false, null)); + ApplyBroadcastDisabled(forcePublish: true); } } else diff --git a/LightlessSync/Services/ContextMenuService.cs b/LightlessSync/Services/ContextMenuService.cs index ad00514..97bfc17 100644 --- a/LightlessSync/Services/ContextMenuService.cs +++ b/LightlessSync/Services/ContextMenuService.cs @@ -147,7 +147,7 @@ internal class ContextMenuService : IHostedService var receiverCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address); _logger.LogInformation("Sending pair request: sender {SenderCid}, receiver {ReceiverCid}", senderCid, receiverCid); - await _apiController.TryPairWithContentId(receiverCid, senderCid).ConfigureAwait(false); + await _apiController.TryPairWithContentId(receiverCid).ConfigureAwait(false); if (!string.IsNullOrWhiteSpace(receiverCid)) { _pairRequestService.RemoveRequest(receiverCid); diff --git a/LightlessSync/UI/BroadcastUI.cs b/LightlessSync/UI/BroadcastUI.cs index 5749a9c..2e2334a 100644 --- a/LightlessSync/UI/BroadcastUI.cs +++ b/LightlessSync/UI/BroadcastUI.cs @@ -209,7 +209,7 @@ namespace LightlessSync.UI else { ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("DimRed")); - ImGui.Text("The Lightfinder’s light wanes, but not in vain."); // cringe.. + ImGui.Text("The Lightfinder’s light wanes, but not in vain."); // cringe.. ImGui.PopStyleColor(); } } diff --git a/LightlessSync/UI/DtrEntry.cs b/LightlessSync/UI/DtrEntry.cs index 0335136..1b3ab61 100644 --- a/LightlessSync/UI/DtrEntry.cs +++ b/LightlessSync/UI/DtrEntry.cs @@ -1,10 +1,11 @@ -using Dalamud.Game.Gui.Dtr; +using Dalamud.Game.Gui.Dtr; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Plugin.Services; using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration.Configurations; using LightlessSync.PlayerData.Pairs; +using LightlessSync.Services; using LightlessSync.Services.Mediator; using LightlessSync.Services.ServerConfiguration; using LightlessSync.WebAPI; @@ -12,6 +13,7 @@ using LightlessSync.WebAPI.SignalR.Utils; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System.Runtime.InteropServices; +using System.Text; namespace LightlessSync.UI; @@ -22,35 +24,52 @@ public sealed class DtrEntry : IDisposable, IHostedService private readonly CancellationTokenSource _cancellationTokenSource = new(); private readonly ConfigurationServiceBase _configService; private readonly IDtrBar _dtrBar; - private readonly Lazy _entry; + private readonly Lazy _statusEntry; + private readonly Lazy _lightfinderEntry; private readonly ILogger _logger; + private readonly BroadcastService _broadcastService; private readonly LightlessMediator _lightlessMediator; private readonly PairManager _pairManager; private Task? _runTask; - private string? _text; - private string? _tooltip; - private Colors _colors; + private string? _statusText; + private string? _statusTooltip; + private Colors _statusColors; + private string? _lightfinderText; + private string? _lightfinderTooltip; + private Colors _lightfinderColors; - public DtrEntry(ILogger logger, IDtrBar dtrBar, ConfigurationServiceBase configService, LightlessMediator lightlessMediator, PairManager pairManager, ApiController apiController, ServerConfigurationManager serverManager) + public DtrEntry( + ILogger logger, + IDtrBar dtrBar, + ConfigurationServiceBase configService, + LightlessMediator lightlessMediator, + PairManager pairManager, + ApiController apiController, + ServerConfigurationManager serverManager, + BroadcastService broadcastService) { _logger = logger; _dtrBar = dtrBar; - _entry = new(CreateEntry); + _statusEntry = new(CreateStatusEntry); + _lightfinderEntry = new(CreateLightfinderEntry); _configService = configService; _lightlessMediator = lightlessMediator; _pairManager = pairManager; _apiController = apiController; _serverManager = serverManager; + _broadcastService = broadcastService; } public void Dispose() { - if (_entry.IsValueCreated) + if (_statusEntry.IsValueCreated) { _logger.LogDebug("Disposing DtrEntry"); Clear(); - _entry.Value.Remove(); + _statusEntry.Value.Remove(); } + if (_lightfinderEntry.IsValueCreated) + _lightfinderEntry.Value.Remove(); } public Task StartAsync(CancellationToken cancellationToken) @@ -70,7 +89,7 @@ public sealed class DtrEntry : IDisposable, IHostedService } catch (OperationCanceledException) { - // ignore cancelled + } finally { @@ -80,33 +99,66 @@ public sealed class DtrEntry : IDisposable, IHostedService private void Clear() { - if (!_entry.IsValueCreated) return; - _logger.LogInformation("Clearing entry"); - _text = null; - _tooltip = null; - _colors = default; - - _entry.Value.Shown = false; + HideStatusEntry(); + HideLightfinderEntry(); } - private IDtrBarEntry CreateEntry() + private void HideStatusEntry() { - _logger.LogTrace("Creating new DtrBar entry"); + if (_statusEntry.IsValueCreated && _statusEntry.Value.Shown) + { + _logger.LogInformation("Hiding status entry"); + _statusEntry.Value.Shown = false; + } + + _statusText = null; + _statusTooltip = null; + _statusColors = default; + } + + private void HideLightfinderEntry() + { + if (_lightfinderEntry.IsValueCreated && _lightfinderEntry.Value.Shown) + { + _logger.LogInformation("Hiding Lightfinder entry"); + _lightfinderEntry.Value.Shown = false; + } + + _lightfinderText = null; + _lightfinderTooltip = null; + _lightfinderColors = default; + } + + private IDtrBarEntry CreateStatusEntry() + { + _logger.LogTrace("Creating status DtrBar entry"); var entry = _dtrBar.Get("Lightless Sync"); - entry.OnClick = interactionEvent => OnClickEvent(interactionEvent); + entry.OnClick = interactionEvent => OnStatusEntryClick(interactionEvent); return entry; } - - private void OnClickEvent(DtrInteractionEvent interactionEvent) + + private IDtrBarEntry CreateLightfinderEntry() { - if (interactionEvent.ClickType.Equals(MouseClickType.Left) && !interactionEvent.ModifierKeys.Equals(ClickModifierKeys.Shift)) + _logger.LogTrace("Creating Lightfinder DtrBar entry"); + var entry = _dtrBar.Get("Lightfinder"); + entry.OnClick = interactionEvent => OnLightfinderEntryClick(interactionEvent); + return entry; + } + + private void OnStatusEntryClick(DtrInteractionEvent interactionEvent) + { + if (interactionEvent.ClickType.Equals(MouseClickType.Left)) { - _lightlessMediator.Publish(new UiToggleMessage(typeof(CompactUi))); - } - else if (interactionEvent.ClickType.Equals(MouseClickType.Left) && interactionEvent.ModifierKeys.Equals(ClickModifierKeys.Shift)) - { - _lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi))); + if (interactionEvent.ModifierKeys.HasFlag(ClickModifierKeys.Shift)) + { + _lightlessMediator.Publish(new UiToggleMessage(typeof(SettingsUi))); + } + else + { + _lightlessMediator.Publish(new UiToggleMessage(typeof(CompactUi))); + } + return; } if (interactionEvent.ClickType.Equals(MouseClickType.Right)) @@ -131,6 +183,17 @@ public sealed class DtrEntry : IDisposable, IHostedService } } + private void OnLightfinderEntryClick(DtrInteractionEvent interactionEvent) + { + if (!_configService.Current.ShowLightfinderInDtr) + return; + + if (interactionEvent.ClickType.Equals(MouseClickType.Left)) + { + _broadcastService.ToggleBroadcast(); + } + } + private async Task RunAsync() { while (!_cancellationTokenSource.IsCancellationRequested) @@ -143,73 +206,171 @@ public sealed class DtrEntry : IDisposable, IHostedService private void Update() { - if (!_configService.Current.EnableDtrEntry || !_configService.Current.HasValidSetup()) - { - if (_entry.IsValueCreated && _entry.Value.Shown) - { - _logger.LogInformation("Disabling entry"); + var config = _configService.Current; - Clear(); - } + if (!config.HasValidSetup()) + { + HideStatusEntry(); + HideLightfinderEntry(); return; } - if (!_entry.Value.Shown) - { - _logger.LogInformation("Showing entry"); - _entry.Value.Shown = true; - } + if (config.EnableDtrEntry) + UpdateStatusEntry(config); + else + HideStatusEntry(); + if (config.ShowLightfinderInDtr) + UpdateLightfinderEntry(config); + else + HideLightfinderEntry(); + } + + private void UpdateStatusEntry(LightlessConfig config) + { string text; string tooltip; Colors colors; + if (_apiController.IsConnected) { var pairCount = _pairManager.GetVisibleUserCount(); text = $"\uE044 {pairCount}"; if (pairCount > 0) { - IEnumerable visiblePairs; - if (_configService.Current.ShowUidInDtrTooltip) - { - visiblePairs = _pairManager.GetOnlineUserPairs() - .Where(x => x.IsVisible) - .Select(x => string.Format("{0} ({1})", _configService.Current.PreferNoteInDtrTooltip ? x.GetNote() ?? x.PlayerName : x.PlayerName, x.UserData.AliasOrUID)); - } - else - { - visiblePairs = _pairManager.GetOnlineUserPairs() - .Where(x => x.IsVisible) - .Select(x => string.Format("{0}", _configService.Current.PreferNoteInDtrTooltip ? x.GetNote() ?? x.PlayerName : x.PlayerName)); - } + var preferNote = config.PreferNoteInDtrTooltip; + var showUid = config.ShowUidInDtrTooltip; + + var visiblePairsQuery = _pairManager.GetOnlineUserPairs() + .Where(x => x.IsVisible); + + IEnumerable visiblePairs = showUid + ? visiblePairsQuery.Select(x => string.Format("{0} ({1})", preferNote ? x.GetNote() ?? x.PlayerName : x.PlayerName, x.UserData.AliasOrUID)) + : visiblePairsQuery.Select(x => string.Format("{0}", preferNote ? x.GetNote() ?? x.PlayerName : x.PlayerName)); tooltip = $"Lightless Sync: Connected{Environment.NewLine}----------{Environment.NewLine}{string.Join(Environment.NewLine, visiblePairs)}"; - colors = _configService.Current.DtrColorsPairsInRange; + colors = config.DtrColorsPairsInRange; } else { tooltip = "Lightless Sync: Connected"; - colors = _configService.Current.DtrColorsDefault; + colors = config.DtrColorsDefault; } } else { text = "\uE044 \uE04C"; tooltip = "Lightless Sync: Not Connected"; - colors = _configService.Current.DtrColorsNotConnected; + colors = config.DtrColorsNotConnected; } - if (!_configService.Current.UseColorsInDtr) + if (!config.UseColorsInDtr) colors = default; - if (!string.Equals(text, _text, StringComparison.Ordinal) || !string.Equals(tooltip, _tooltip, StringComparison.Ordinal) || colors != _colors) + var statusEntry = _statusEntry.Value; + if (!statusEntry.Shown) { - _text = text; - _tooltip = tooltip; - _colors = colors; - _entry.Value.Text = BuildColoredSeString(text, colors); - _entry.Value.Tooltip = tooltip; + _logger.LogInformation("Showing status entry"); + statusEntry.Shown = true; } + + bool statusNeedsUpdate = + !string.Equals(text, _statusText, StringComparison.Ordinal) || + !string.Equals(tooltip, _statusTooltip, StringComparison.Ordinal) || + colors != _statusColors; + + if (statusNeedsUpdate) + { + statusEntry.Text = BuildColoredSeString(text, colors); + statusEntry.Tooltip = tooltip; + _statusText = text; + _statusTooltip = tooltip; + _statusColors = colors; + } + } + + private void UpdateLightfinderEntry(LightlessConfig config) + { + var lightfinderEntry = _lightfinderEntry.Value; + if (!lightfinderEntry.Shown) + { + _logger.LogInformation("Showing Lightfinder entry"); + lightfinderEntry.Shown = true; + } + + var indicator = BuildLightfinderIndicator(); + var lightfinderText = indicator.Text ?? string.Empty; + var lightfinderColors = config.UseLightfinderColorsInDtr ? indicator.Colors : default; + var lightfinderTooltip = BuildLightfinderTooltip(indicator.Tooltip); + + bool lightfinderNeedsUpdate = + !string.Equals(lightfinderText, _lightfinderText, StringComparison.Ordinal) || + !string.Equals(lightfinderTooltip, _lightfinderTooltip, StringComparison.Ordinal) || + lightfinderColors != _lightfinderColors; + + if (lightfinderNeedsUpdate) + { + lightfinderEntry.Text = BuildColoredSeString(lightfinderText, lightfinderColors); + lightfinderEntry.Tooltip = lightfinderTooltip; + _lightfinderText = lightfinderText; + _lightfinderTooltip = lightfinderTooltip; + _lightfinderColors = lightfinderColors; + } + } + + 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."); + } + + if (_broadcastService.IsBroadcasting) + { + return ($"{icon} ON", config.DtrColorsLightfinderEnabled, "Lightfinder - Enabled"); + } + + var tooltip = new StringBuilder("Lightfinder - Disabled"); + var colors = 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; + } + + return ($"{icon} OFF", colors, tooltip.ToString()); + } + + private static string BuildLightfinderTooltip(string baseTooltip) + { + var builder = new StringBuilder(); + if (!string.IsNullOrWhiteSpace(baseTooltip)) + builder.Append(baseTooltip.TrimEnd()); + else + builder.Append("Lightfinder status unavailable."); + + return builder.ToString().TrimEnd(); + } + + private static void AppendColoredSegment(SeStringBuilder builder, string? text, Colors colors) + { + if (string.IsNullOrEmpty(text)) + return; + + if (colors.Foreground != default) + builder.Add(BuildColorStartPayload(_colorTypeForeground, colors.Foreground)); + if (colors.Glow != default) + builder.Add(BuildColorStartPayload(_colorTypeGlow, colors.Glow)); + + builder.AddText(text); + + if (colors.Glow != default) + builder.Add(BuildColorEndPayload(_colorTypeGlow)); + if (colors.Foreground != default) + builder.Add(BuildColorEndPayload(_colorTypeForeground)); } #region Colored SeString @@ -219,15 +380,7 @@ public sealed class DtrEntry : IDisposable, IHostedService private static SeString BuildColoredSeString(string text, Colors colors) { var ssb = new SeStringBuilder(); - if (colors.Foreground != default) - ssb.Add(BuildColorStartPayload(_colorTypeForeground, colors.Foreground)); - if (colors.Glow != default) - ssb.Add(BuildColorStartPayload(_colorTypeGlow, colors.Glow)); - ssb.AddText(text); - if (colors.Glow != default) - ssb.Add(BuildColorEndPayload(_colorTypeGlow)); - if (colors.Foreground != default) - ssb.Add(BuildColorEndPayload(_colorTypeForeground)); + AppendColoredSegment(ssb, text, colors); return ssb.Build(); } diff --git a/LightlessSync/UI/SettingsUi.cs b/LightlessSync/UI/SettingsUi.cs index 84e4e94..52de306 100644 --- a/LightlessSync/UI/SettingsUi.cs +++ b/LightlessSync/UI/SettingsUi.cs @@ -181,12 +181,13 @@ public class SettingsUi : WindowMediatorSubscriberBase var foregroundColor = ConvertColor(colors.Foreground); var glowColor = ConvertColor(colors.Glow); - var ret = ImGui.ColorEdit3("###foreground", ref foregroundColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.NoLabel | ImGuiColorEditFlags.Uint8); + const ImGuiColorEditFlags colorFlags = ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.NoLabel; + var ret = ImGui.ColorEdit3("###foreground", ref foregroundColor, colorFlags); if (ImGui.IsItemHovered()) ImGui.SetTooltip("Foreground Color - Set to pure black (#000000) to use the default color"); ImGui.SameLine(0.0f, innerSpacing); - ret |= ImGui.ColorEdit3("###glow", ref glowColor, ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.NoLabel | ImGuiColorEditFlags.Uint8); + ret |= ImGui.ColorEdit3("###glow", ref glowColor, colorFlags); if (ImGui.IsItemHovered()) ImGui.SetTooltip("Glow Color - Set to pure black (#000000) to use the default color"); @@ -199,10 +200,26 @@ public class SettingsUi : WindowMediatorSubscriberBase return ret; static Vector3 ConvertColor(uint color) - => unchecked(new((byte)color / 255.0f, (byte)(color >> 8) / 255.0f, (byte)(color >> 16) / 255.0f)); + { + var r = (color & 0xFF) / 255.0f; + var g = ((color >> 8) & 0xFF) / 255.0f; + var b = ((color >> 16) & 0xFF) / 255.0f; + return new(r, g, b); + } static uint ConvertBackColor(Vector3 color) - => byte.CreateSaturating(color.X * 255.0f) | ((uint)byte.CreateSaturating(color.Y * 255.0f) << 8) | ((uint)byte.CreateSaturating(color.Z * 255.0f) << 16); + { + static byte ToByte(float channel) + { + var scaled = MathF.Round(Math.Clamp(channel, 0f, 1f) * 255.0f); + return (byte)Math.Clamp((int)scaled, 0, 255); + } + + var r = ToByte(color.X); + var g = ToByte(color.Y); + var b = ToByte(color.Z); + return (uint)(r | (g << 8) | (b << 16)); + } } private void DrawBlockedTransfers() @@ -1066,10 +1083,66 @@ public class SettingsUi : WindowMediatorSubscriberBase if (_uiShared.MediumTreeNode("Lightfinder", UIColors.Get("LightlessPurple"))) { + 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; + + ImGui.TextUnformatted("Connection"); + if (ImGui.Checkbox("Auto-enable Lightfinder on server connection", ref autoEnable)) + { + _configService.Current.LightfinderAutoEnableOnConnect = autoEnable; + _configService.Save(); + } + _uiShared.DrawHelpText("When enabled, Lightfinder will automatically turn on after reconnecting to the Lightless server."); + + _uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); + + ImGui.TextUnformatted("Lightfinder Info Bar"); + if (ImGui.Checkbox("Show Lightfinder status in Server info bar", ref showLightfinderInDtr)) + { + _configService.Current.ShowLightfinderInDtr = showLightfinderInDtr; + _configService.Save(); + } + _uiShared.DrawHelpText("Adds a Lightfinder status to the Server info bar. Left click toggles Lightfinder when visible."); + + bool useLightfinderColors = _configService.Current.UseLightfinderColorsInDtr; + if (ImGui.Checkbox("Color-code the Lightfinder info bar according to status", ref useLightfinderColors)) + { + _configService.Current.UseLightfinderColorsInDtr = useLightfinderColors; + _configService.Save(); + } + + ImGui.BeginDisabled(!showLightfinderInDtr || !useLightfinderColors); + if (InputDtrColors("Enables", ref dtrLightfinderEnabled)) + { + _configService.Current.DtrColorsLightfinderEnabled = dtrLightfinderEnabled; + _configService.Save(); + } + if (InputDtrColors("Disabled", ref dtrLightfinderDisabled)) + { + _configService.Current.DtrColorsLightfinderDisabled = dtrLightfinderDisabled; + _configService.Save(); + } + if (InputDtrColors("Cooldown", ref dtrLightfinderCooldown)) + { + _configService.Current.DtrColorsLightfinderCooldown = dtrLightfinderCooldown; + _configService.Save(); + } + if (InputDtrColors("Unavailable", ref dtrLightfinderUnavailable)) + { + _configService.Current.DtrColorsLightfinderUnavailable = dtrLightfinderUnavailable; + _configService.Save(); + } + ImGui.EndDisabled(); + + _uiShared.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f); ImGui.TextUnformatted("Alignment"); ImGui.BeginDisabled(autoAlign); diff --git a/LightlessSync/UI/TopTabMenu.cs b/LightlessSync/UI/TopTabMenu.cs index 8a9e6c9..47f8f86 100644 --- a/LightlessSync/UI/TopTabMenu.cs +++ b/LightlessSync/UI/TopTabMenu.cs @@ -401,7 +401,7 @@ public class TopTabMenu try { var myCidHash = (await _dalamudUtilService.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256(); - await _apiController.TryPairWithContentId(request.HashedCid, myCidHash).ConfigureAwait(false); + await _apiController.TryPairWithContentId(request.HashedCid).ConfigureAwait(false); _pairRequestService.RemoveRequest(request.HashedCid); var display = string.IsNullOrEmpty(request.DisplayName) ? request.HashedCid : request.DisplayName; diff --git a/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs b/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs index d268dd8..bbac3a6 100644 --- a/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs +++ b/LightlessSync/WebAPI/SignalR/ApIController.Functions.Users.cs @@ -35,16 +35,16 @@ public partial class ApiController await _lightlessHub!.SendAsync(nameof(UserAddPair), user).ConfigureAwait(false); } - public async Task TryPairWithContentId(string otherCid, string myCid) + public async Task TryPairWithContentId(string otherCid) { if (!IsConnected) return; - await _lightlessHub!.SendAsync(nameof(TryPairWithContentId), otherCid, myCid).ConfigureAwait(false); + await _lightlessHub!.SendAsync(nameof(TryPairWithContentId), otherCid).ConfigureAwait(false); } - public async Task SetBroadcastStatus(string hashedCid, bool enabled, GroupBroadcastRequestDto? groupDto = null) + public async Task SetBroadcastStatus(bool enabled, GroupBroadcastRequestDto? groupDto = null) { CheckConnection(); - await _lightlessHub!.InvokeAsync(nameof(SetBroadcastStatus), hashedCid, enabled, groupDto).ConfigureAwait(false); + await _lightlessHub!.InvokeAsync(nameof(SetBroadcastStatus), enabled, groupDto).ConfigureAwait(false); } public async Task IsUserBroadcasting(string hashedCid) @@ -59,10 +59,10 @@ public partial class ApiController return await _lightlessHub!.InvokeAsync(nameof(AreUsersBroadcasting), hashedCids).ConfigureAwait(false); } - public async Task GetBroadcastTtl(string hashedCid) + public async Task GetBroadcastTtl() { CheckConnection(); - return await _lightlessHub!.InvokeAsync(nameof(GetBroadcastTtl), hashedCid).ConfigureAwait(false); + return await _lightlessHub!.InvokeAsync(nameof(GetBroadcastTtl)).ConfigureAwait(false); } public async Task UserDelete() diff --git a/PenumbraAPI b/PenumbraAPI index dd14131..648b6fc 160000 --- a/PenumbraAPI +++ b/PenumbraAPI @@ -1 +1 @@ -Subproject commit dd14131793e5ae47cc8e9232f46469216017b5aa +Subproject commit 648b6fc2ce600a95ab2b2ced27e1639af2b04502