530 lines
18 KiB
C#
530 lines
18 KiB
C#
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.Services;
|
|
using LightlessSync.Services.Mediator;
|
|
using LightlessSync.Services.ServerConfiguration;
|
|
using LightlessSync.Utils;
|
|
using LightlessSync.WebAPI;
|
|
using LightlessSync.WebAPI.SignalR.Utils;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Primitives;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using LightlessSync.UI.Services;
|
|
using LightlessSync.PlayerData.Pairs;
|
|
using static LightlessSync.Services.PairRequestService;
|
|
using LightlessSync.Services.LightFinder;
|
|
|
|
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();
|
|
private readonly ConfigurationServiceBase<LightlessConfig> _configService;
|
|
private readonly IDtrBar _dtrBar;
|
|
private readonly Lazy<IDtrBarEntry> _statusEntry;
|
|
private readonly Lazy<IDtrBarEntry> _lightfinderEntry;
|
|
private readonly ILogger<DtrEntry> _logger;
|
|
private readonly LightFinderService _broadcastService;
|
|
private readonly LightFinderScannerService _broadcastScannerService;
|
|
private readonly LightlessMediator _lightlessMediator;
|
|
private readonly PairUiService _pairUiService;
|
|
private readonly PairRequestService _pairRequestService;
|
|
private readonly DalamudUtilService _dalamudUtilService;
|
|
private Task? _runTask;
|
|
private string? _statusText;
|
|
private string? _statusTooltip;
|
|
private Colors _statusColors;
|
|
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<DtrEntry> logger,
|
|
IDtrBar dtrBar,
|
|
ConfigurationServiceBase<LightlessConfig> configService,
|
|
LightlessMediator lightlessMediator,
|
|
PairUiService pairUiService,
|
|
PairRequestService pairRequestService,
|
|
ApiController apiController,
|
|
ServerConfigurationManager serverManager,
|
|
LightFinderService broadcastService,
|
|
LightFinderScannerService broadcastScannerService,
|
|
DalamudUtilService dalamudUtilService)
|
|
{
|
|
_logger = logger;
|
|
_dtrBar = dtrBar;
|
|
_statusEntry = new(CreateStatusEntry);
|
|
_lightfinderEntry = new(CreateLightfinderEntry);
|
|
_configService = configService;
|
|
_lightlessMediator = lightlessMediator;
|
|
_pairUiService = pairUiService;
|
|
_pairRequestService = pairRequestService;
|
|
_apiController = apiController;
|
|
_serverManager = serverManager;
|
|
_broadcastService = broadcastService;
|
|
_broadcastScannerService = broadcastScannerService;
|
|
_dalamudUtilService = dalamudUtilService;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_statusEntry.IsValueCreated)
|
|
{
|
|
_logger.LogDebug("Disposing DtrEntry");
|
|
Clear();
|
|
_statusEntry.Value.Remove();
|
|
}
|
|
if (_lightfinderEntry.IsValueCreated)
|
|
_lightfinderEntry.Value.Remove();
|
|
}
|
|
|
|
public Task StartAsync(CancellationToken cancellationToken)
|
|
{
|
|
_logger.LogInformation("Starting DtrEntry");
|
|
_runTask = Task.Run(RunAsync, _cancellationTokenSource.Token);
|
|
_logger.LogInformation("Started DtrEntry");
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public async Task StopAsync(CancellationToken cancellationToken)
|
|
{
|
|
await _cancellationTokenSource.CancelAsync().ConfigureAwait(false);
|
|
try
|
|
{
|
|
await _runTask!.ConfigureAwait(false);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
_logger.LogInformation("Lightfinder operation was canceled.");
|
|
}
|
|
finally
|
|
{
|
|
_cancellationTokenSource.Dispose();
|
|
}
|
|
}
|
|
|
|
private void Clear()
|
|
{
|
|
HideStatusEntry();
|
|
HideLightfinderEntry();
|
|
}
|
|
|
|
private void HideStatusEntry()
|
|
{
|
|
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 => OnStatusEntryClick(interactionEvent);
|
|
|
|
return entry;
|
|
}
|
|
|
|
private IDtrBarEntry CreateLightfinderEntry()
|
|
{
|
|
_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))
|
|
{
|
|
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))
|
|
{
|
|
bool isConnectingOrConnected = _apiController.ServerState is ServerState.Connected or ServerState.Connecting or ServerState.Reconnecting;
|
|
|
|
if (_apiController.ServerState is not (ServerState.Reconnecting or ServerState.Disconnecting))
|
|
{
|
|
if (isConnectingOrConnected && !_serverManager.CurrentServer.FullPause)
|
|
{
|
|
_serverManager.CurrentServer.FullPause = true;
|
|
_serverManager.Save();
|
|
}
|
|
else if (!isConnectingOrConnected && _serverManager.CurrentServer.FullPause)
|
|
{
|
|
_serverManager.CurrentServer.FullPause = false;
|
|
_serverManager.Save();
|
|
}
|
|
|
|
_ = _apiController.CreateConnectionsAsync();
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
await Task.Delay(1000, _cancellationTokenSource.Token).ConfigureAwait(false);
|
|
|
|
Update();
|
|
}
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
var config = _configService.Current;
|
|
|
|
if (!config.HasValidSetup())
|
|
{
|
|
HideStatusEntry();
|
|
HideLightfinderEntry();
|
|
return;
|
|
}
|
|
|
|
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 snapshot = _pairUiService.GetSnapshot();
|
|
var visiblePairsQuery = snapshot.PairsByUid.Values.Where(x => x.IsVisible && !x.IsPaused);
|
|
var pairCount = visiblePairsQuery.Count();
|
|
text = $"\uE044 {pairCount}";
|
|
if (pairCount > 0)
|
|
{
|
|
var preferNote = config.PreferNoteInDtrTooltip;
|
|
var showUid = config.ShowUidInDtrTooltip;
|
|
|
|
IEnumerable<string> 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 = config.DtrColorsPairsInRange;
|
|
}
|
|
else
|
|
{
|
|
tooltip = "Lightless Sync: Connected";
|
|
colors = config.DtrColorsDefault;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
text = "\uE044 \uE04C";
|
|
tooltip = "Lightless Sync: Not Connected";
|
|
colors = config.DtrColorsNotConnected;
|
|
}
|
|
|
|
if (!config.UseColorsInDtr)
|
|
colors = default;
|
|
|
|
var statusEntry = _statusEntry.Value;
|
|
if (!statusEntry.Shown)
|
|
{
|
|
_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? 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().GetBlake3Hash();
|
|
_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 List<string> GetNearbyBroadcasts()
|
|
{
|
|
try
|
|
{
|
|
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<PairRequestDisplay> 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 [];
|
|
}
|
|
}
|
|
|
|
private (string Text, Colors Colors, string Tooltip) BuildLightfinderIndicator()
|
|
{
|
|
var config = _configService.Current;
|
|
const string icon = "\uE048";
|
|
if (!_broadcastService.IsLightFinderAvailable)
|
|
{
|
|
return ($"{icon} --", SwapColorChannels(config.DtrColorsLightfinderUnavailable), "Lightfinder - Unavailable on this server.");
|
|
}
|
|
|
|
if (_broadcastService.IsBroadcasting)
|
|
{
|
|
switch (config.LightfinderDtrDisplayMode)
|
|
{
|
|
case LightfinderDtrDisplayMode.PendingPairRequests:
|
|
{
|
|
return FormatTooltip("Pending pair requests", GetPendingPairRequest().Select(x => x.DisplayName), icon, SwapColorChannels(config.DtrColorsLightfinderEnabled));
|
|
}
|
|
default:
|
|
{
|
|
return FormatTooltip("Nearby Lightfinder users", GetNearbyBroadcasts(), icon, SwapColorChannels(config.DtrColorsLightfinderEnabled));
|
|
}
|
|
}
|
|
}
|
|
|
|
var tooltip = new StringBuilder("Lightfinder - Disabled");
|
|
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 = SwapColorChannels(config.DtrColorsLightfinderCooldown);
|
|
}
|
|
|
|
return ($"{icon} OFF", colors, tooltip.ToString());
|
|
}
|
|
|
|
private static (string, Colors, string) FormatTooltip(string title, IEnumerable<string> 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();
|
|
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
|
|
private const byte _colorTypeForeground = 0x13;
|
|
private const byte _colorTypeGlow = 0x14;
|
|
|
|
internal 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();
|
|
AppendColoredSegment(ssb, text, colors);
|
|
return ssb.Build();
|
|
}
|
|
|
|
private static RawPayload BuildColorStartPayload(byte colorType, uint color)
|
|
=> 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]);
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public readonly record struct Colors(uint Foreground = default, uint Glow = default);
|
|
#endregion
|
|
} |