seperate scanning service not relying on nameplate updates & other improvements/fixes
This commit is contained in:
@@ -205,6 +205,8 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
collection.AddSingleton<ConfigurationSaveService>();
|
collection.AddSingleton<ConfigurationSaveService>();
|
||||||
collection.AddSingleton<HubFactory>();
|
collection.AddSingleton<HubFactory>();
|
||||||
collection.AddSingleton<NameplateHandler>();
|
collection.AddSingleton<NameplateHandler>();
|
||||||
|
collection.AddSingleton(s => new BroadcastScannerService( s.GetRequiredService<ILogger<BroadcastScannerService>>(), clientState, objectTable, framework, s.GetRequiredService<BroadcastService>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<NameplateHandler>(), s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<LightlessConfigService>()));
|
||||||
|
|
||||||
|
|
||||||
// add scoped services
|
// add scoped services
|
||||||
collection.AddScoped<DrawEntityFactory>();
|
collection.AddScoped<DrawEntityFactory>();
|
||||||
@@ -227,8 +229,8 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<UiSharedService>(), s.GetRequiredService<FileDialogManager>(),
|
s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<UiSharedService>(), s.GetRequiredService<FileDialogManager>(),
|
||||||
s.GetRequiredService<LightlessProfileManager>(), s.GetRequiredService<PerformanceCollectorService>()));
|
s.GetRequiredService<LightlessProfileManager>(), s.GetRequiredService<PerformanceCollectorService>()));
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, PopupHandler>();
|
collection.AddScoped<WindowMediatorSubscriberBase, PopupHandler>();
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, BroadcastUI>((s) => new BroadcastUI(s.GetRequiredService<ILogger<BroadcastUI>>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PerformanceCollectorService>(), s.GetRequiredService<BroadcastService>(), s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<UiSharedService>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<NameplateService>()));
|
collection.AddScoped<WindowMediatorSubscriberBase, BroadcastUI>((s) => new BroadcastUI(s.GetRequiredService<ILogger<BroadcastUI>>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PerformanceCollectorService>(), s.GetRequiredService<BroadcastService>(), s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<UiSharedService>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<BroadcastScannerService>()));
|
||||||
collection.AddScoped<WindowMediatorSubscriberBase, SyncshellFinderUI>((s) => new SyncshellFinderUI(s.GetRequiredService<ILogger<SyncshellFinderUI>>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PerformanceCollectorService>(), s.GetRequiredService<BroadcastService>(), s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<UiSharedService>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<NameplateService>()));
|
collection.AddScoped<WindowMediatorSubscriberBase, SyncshellFinderUI>((s) => new SyncshellFinderUI(s.GetRequiredService<ILogger<SyncshellFinderUI>>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<PerformanceCollectorService>(), s.GetRequiredService<BroadcastService>(), s.GetRequiredService<LightlessConfigService>(), s.GetRequiredService<UiSharedService>(), s.GetRequiredService<ApiController>(), s.GetRequiredService<BroadcastScannerService>()));
|
||||||
collection.AddScoped<IPopupHandler, BanUserPopupHandler>();
|
collection.AddScoped<IPopupHandler, BanUserPopupHandler>();
|
||||||
collection.AddScoped<IPopupHandler, CensusPopupHandler>();
|
collection.AddScoped<IPopupHandler, CensusPopupHandler>();
|
||||||
collection.AddScoped<CacheCreationService>();
|
collection.AddScoped<CacheCreationService>();
|
||||||
@@ -246,7 +248,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
pluginInterface, textureProvider, s.GetRequiredService<Dalamud.Localization>(), s.GetRequiredService<ServerConfigurationManager>(), s.GetRequiredService<TokenProvider>(),
|
pluginInterface, textureProvider, s.GetRequiredService<Dalamud.Localization>(), s.GetRequiredService<ServerConfigurationManager>(), s.GetRequiredService<TokenProvider>(),
|
||||||
s.GetRequiredService<LightlessMediator>()));
|
s.GetRequiredService<LightlessMediator>()));
|
||||||
collection.AddScoped((s) => new NameplateService(s.GetRequiredService<ILogger<NameplateService>>(), s.GetRequiredService<LightlessConfigService>(), namePlateGui, clientState,
|
collection.AddScoped((s) => new NameplateService(s.GetRequiredService<ILogger<NameplateService>>(), s.GetRequiredService<LightlessConfigService>(), namePlateGui, clientState,
|
||||||
s.GetRequiredService<PairManager>(), s.GetRequiredService<BroadcastService>(), s.GetRequiredService<LightlessMediator>(), s.GetRequiredService<DalamudUtilService>(), s.GetRequiredService<NameplateHandler>(), s.GetRequiredService<IGameGui>()));
|
s.GetRequiredService<PairManager>(), s.GetRequiredService<LightlessMediator>()));
|
||||||
|
|
||||||
collection.AddHostedService(p => p.GetRequiredService<ConfigurationSaveService>());
|
collection.AddHostedService(p => p.GetRequiredService<ConfigurationSaveService>());
|
||||||
collection.AddHostedService(p => p.GetRequiredService<LightlessMediator>());
|
collection.AddHostedService(p => p.GetRequiredService<LightlessMediator>());
|
||||||
|
|||||||
216
LightlessSync/Services/BroadcastScanningService.cs
Normal file
216
LightlessSync/Services/BroadcastScanningService.cs
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using LightlessSync.API.Dto.User;
|
||||||
|
using LightlessSync.LightlessConfiguration;
|
||||||
|
using LightlessSync.Services.Mediator;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace LightlessSync.Services;
|
||||||
|
|
||||||
|
public class BroadcastScannerService : DisposableMediatorSubscriberBase, IDisposable
|
||||||
|
{
|
||||||
|
private readonly ILogger<BroadcastScannerService> _logger;
|
||||||
|
private readonly IObjectTable _objectTable;
|
||||||
|
private readonly IFramework _framework;
|
||||||
|
|
||||||
|
private readonly BroadcastService _broadcastService;
|
||||||
|
private readonly NameplateHandler _nameplateHandler;
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<string, BroadcastEntry> _broadcastCache = new();
|
||||||
|
private readonly Queue<string> _lookupQueue = new();
|
||||||
|
private readonly HashSet<string> _lookupQueuedCids = new();
|
||||||
|
private readonly HashSet<string> _syncshellCids = new();
|
||||||
|
|
||||||
|
private static readonly TimeSpan MaxAllowedTtl = TimeSpan.FromMinutes(4);
|
||||||
|
private static readonly TimeSpan RetryDelay = TimeSpan.FromMinutes(1);
|
||||||
|
|
||||||
|
private readonly CancellationTokenSource _cleanupCts = new();
|
||||||
|
private Task? _cleanupTask;
|
||||||
|
|
||||||
|
private int _frameCounter = 0;
|
||||||
|
private int _lookupsThisFrame = 0;
|
||||||
|
private const int MaxLookupsPerFrame = 15;
|
||||||
|
private const int MaxQueueSize = 100;
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, BroadcastEntry> BroadcastCache => _broadcastCache;
|
||||||
|
public readonly record struct BroadcastEntry(bool IsBroadcasting, DateTime ExpiryTime, string? GID);
|
||||||
|
|
||||||
|
public BroadcastScannerService(ILogger<BroadcastScannerService> logger,
|
||||||
|
IClientState clientState,
|
||||||
|
IObjectTable objectTable,
|
||||||
|
IFramework framework,
|
||||||
|
BroadcastService broadcastService,
|
||||||
|
LightlessMediator mediator,
|
||||||
|
NameplateHandler nameplateHandler,
|
||||||
|
DalamudUtilService dalamudUtil,
|
||||||
|
LightlessConfigService configService) : base(logger, mediator)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_objectTable = objectTable;
|
||||||
|
_broadcastService = broadcastService;
|
||||||
|
_nameplateHandler = nameplateHandler;
|
||||||
|
|
||||||
|
_logger = logger;
|
||||||
|
_framework = framework;
|
||||||
|
_framework.Update += OnFrameworkUpdate;
|
||||||
|
|
||||||
|
Mediator.Subscribe<BroadcastStatusChangedMessage>(this, OnBroadcastStatusChanged);
|
||||||
|
_cleanupTask = Task.Run(ExpiredBroadcastCleanupLoop);
|
||||||
|
|
||||||
|
_nameplateHandler.Init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFrameworkUpdate(IFramework framework) => Update();
|
||||||
|
|
||||||
|
public void Update()
|
||||||
|
{
|
||||||
|
_frameCounter++;
|
||||||
|
_lookupsThisFrame = 0;
|
||||||
|
|
||||||
|
if (!_broadcastService.IsBroadcasting)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
|
foreach (var obj in _objectTable)
|
||||||
|
{
|
||||||
|
if (obj is not IPlayerCharacter player || player.Address == IntPtr.Zero)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer(player.Address);
|
||||||
|
var isStale = !_broadcastCache.TryGetValue(cid, out var entry) || entry.ExpiryTime <= now;
|
||||||
|
|
||||||
|
if (isStale && _lookupQueuedCids.Add(cid) && _lookupQueue.Count < MaxQueueSize)
|
||||||
|
_lookupQueue.Enqueue(cid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_frameCounter % 2 == 0 && _lookupQueue.Count > 0)
|
||||||
|
{
|
||||||
|
var cidsToLookup = new List<string>();
|
||||||
|
while (_lookupQueue.Count > 0 && _lookupsThisFrame < MaxLookupsPerFrame)
|
||||||
|
{
|
||||||
|
var cid = _lookupQueue.Dequeue();
|
||||||
|
_lookupQueuedCids.Remove(cid);
|
||||||
|
cidsToLookup.Add(cid);
|
||||||
|
_lookupsThisFrame++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cidsToLookup.Count > 0)
|
||||||
|
_ = BatchUpdateBroadcastCacheAsync(cidsToLookup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task BatchUpdateBroadcastCacheAsync(List<string> cids)
|
||||||
|
{
|
||||||
|
var results = await _broadcastService.AreUsersBroadcastingAsync(cids).ConfigureAwait(false);
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
|
foreach (var (cid, info) in results)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(cid) || info == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var ttl = info.IsBroadcasting && info.TTL.HasValue
|
||||||
|
? TimeSpan.FromTicks(Math.Min(info.TTL.Value.Ticks, MaxAllowedTtl.Ticks))
|
||||||
|
: RetryDelay;
|
||||||
|
|
||||||
|
var expiry = now + ttl;
|
||||||
|
|
||||||
|
_broadcastCache.AddOrUpdate(cid,
|
||||||
|
new BroadcastEntry(info.IsBroadcasting, expiry, info.GID),
|
||||||
|
(_, old) => new BroadcastEntry(info.IsBroadcasting, expiry, info.GID));
|
||||||
|
}
|
||||||
|
|
||||||
|
var activeCids = _broadcastCache
|
||||||
|
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now)
|
||||||
|
.Select(e => e.Key)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
_nameplateHandler.UpdateBroadcastingCids(activeCids);
|
||||||
|
UpdateSyncshellBroadcasts();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnBroadcastStatusChanged(BroadcastStatusChangedMessage msg)
|
||||||
|
{
|
||||||
|
if (!msg.Enabled)
|
||||||
|
{
|
||||||
|
_broadcastCache.Clear();
|
||||||
|
_lookupQueue.Clear();
|
||||||
|
_lookupQueuedCids.Clear();
|
||||||
|
_syncshellCids.Clear();
|
||||||
|
|
||||||
|
_nameplateHandler.UpdateBroadcastingCids(Enumerable.Empty<string>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateSyncshellBroadcasts()
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var newSet = _broadcastCache
|
||||||
|
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
|
||||||
|
.Select(e => e.Key)
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
if (!_syncshellCids.SetEquals(newSet))
|
||||||
|
{
|
||||||
|
_syncshellCids.Clear();
|
||||||
|
foreach (var cid in newSet)
|
||||||
|
_syncshellCids.Add(cid);
|
||||||
|
|
||||||
|
Mediator.Publish(new SyncshellBroadcastsUpdatedMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<BroadcastStatusInfoDto> GetActiveSyncshellBroadcasts()
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
|
return _broadcastCache
|
||||||
|
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
|
||||||
|
.Select(e => new BroadcastStatusInfoDto
|
||||||
|
{
|
||||||
|
HashedCID = e.Key,
|
||||||
|
IsBroadcasting = true,
|
||||||
|
TTL = e.Value.ExpiryTime - now,
|
||||||
|
GID = e.Value.GID
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExpiredBroadcastCleanupLoop()
|
||||||
|
{
|
||||||
|
var token = _cleanupCts.Token;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (!token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(10), token);
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
foreach (var (cid, entry) in _broadcastCache.ToArray())
|
||||||
|
{
|
||||||
|
if (entry.ExpiryTime <= now)
|
||||||
|
_broadcastCache.TryRemove(cid, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Broadcast cleanup loop crashed");
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateSyncshellBroadcasts();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
_framework.Update -= OnFrameworkUpdate;
|
||||||
|
_cleanupCts.Cancel();
|
||||||
|
_cleanupTask?.Wait(100);
|
||||||
|
_nameplateHandler.Uninit();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -151,6 +151,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
|||||||
_config.Save();
|
_config.Save();
|
||||||
|
|
||||||
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
_mediator.Publish(new BroadcastStatusChangedMessage(false, null));
|
||||||
|
Mediator.Publish(new EventMessage(new Services.Events.Event(nameof(BroadcastService), Services.Events.EventSeverity.Informational,$"Disabled Lightfinder for Player: {msg.HashedCid}")));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +167,7 @@ public class BroadcastService : IHostedService, IMediatorSubscriber
|
|||||||
|
|
||||||
_logger.LogInformation("Fetched TTL from server: {TTL}", remaining);
|
_logger.LogInformation("Fetched TTL from server: {TTL}", remaining);
|
||||||
_mediator.Publish(new BroadcastStatusChangedMessage(true, 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}")));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
|||||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
|
using LightlessSync.UI;
|
||||||
using LightlessSync.Utils;
|
using LightlessSync.Utils;
|
||||||
// Created using https://github.com/PunishedPineapple/Distance as a reference, thank you!
|
// Created using https://github.com/PunishedPineapple/Distance as a reference, thank you!
|
||||||
|
|
||||||
@@ -205,6 +206,9 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
|||||||
var nameContainer = nameplateObject.NameContainer;
|
var nameContainer = nameplateObject.NameContainer;
|
||||||
var nameText = nameplateObject.NameText;
|
var nameText = nameplateObject.NameText;
|
||||||
|
|
||||||
|
var labelColor = UIColors.Get("LightlessPurple");
|
||||||
|
var edgeColor = UIColors.Get("FullBlack");
|
||||||
|
|
||||||
var labelY = nameContainer->Height - nameplateObject.TextH - (int)(24 * nameText->AtkResNode.ScaleY);
|
var labelY = nameContainer->Height - nameplateObject.TextH - (int)(24 * nameText->AtkResNode.ScaleY);
|
||||||
|
|
||||||
pNode->AtkResNode.SetPositionShort(58, (short)labelY);
|
pNode->AtkResNode.SetPositionShort(58, (short)labelY);
|
||||||
@@ -213,15 +217,15 @@ public unsafe class NameplateHandler : IMediatorSubscriber
|
|||||||
|
|
||||||
pNode->AtkResNode.Color.A = 255;
|
pNode->AtkResNode.Color.A = 255;
|
||||||
|
|
||||||
pNode->TextColor.A = 255;
|
pNode->TextColor.R = (byte)(labelColor.X * 255);
|
||||||
pNode->TextColor.R = 173;
|
pNode->TextColor.G = (byte)(labelColor.Y * 255);
|
||||||
pNode->TextColor.G = 138;
|
pNode->TextColor.B = (byte)(labelColor.Z * 255);
|
||||||
pNode->TextColor.B = 245;
|
pNode->TextColor.A = (byte)(labelColor.W * 255);
|
||||||
|
|
||||||
pNode->EdgeColor.A = 255;
|
pNode->EdgeColor.R = (byte)(edgeColor.X * 255);
|
||||||
pNode->EdgeColor.R = 0;
|
pNode->EdgeColor.G = (byte)(edgeColor.Y * 255);
|
||||||
pNode->EdgeColor.G = 0;
|
pNode->EdgeColor.B = (byte)(edgeColor.Z * 255);
|
||||||
pNode->EdgeColor.B = 0;
|
pNode->EdgeColor.A = (byte)(edgeColor.W * 255);
|
||||||
|
|
||||||
pNode->FontSize = 24;
|
pNode->FontSize = 24;
|
||||||
pNode->AlignmentType = AlignmentType.Center;
|
pNode->AlignmentType = AlignmentType.Center;
|
||||||
|
|||||||
@@ -3,13 +3,11 @@ using Dalamud.Game.Gui.NamePlate;
|
|||||||
using Dalamud.Game.Text.SeStringHandling;
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
using LightlessSync.API.Dto.User;
|
|
||||||
using LightlessSync.LightlessConfiguration;
|
using LightlessSync.LightlessConfiguration;
|
||||||
using LightlessSync.PlayerData.Pairs;
|
using LightlessSync.PlayerData.Pairs;
|
||||||
using LightlessSync.Services.Mediator;
|
using LightlessSync.Services.Mediator;
|
||||||
using LightlessSync.UI;
|
using LightlessSync.UI;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
|
|
||||||
namespace LightlessSync.Services;
|
namespace LightlessSync.Services;
|
||||||
|
|
||||||
@@ -20,80 +18,28 @@ public class NameplateService : DisposableMediatorSubscriberBase
|
|||||||
private readonly IClientState _clientState;
|
private readonly IClientState _clientState;
|
||||||
private readonly INamePlateGui _namePlateGui;
|
private readonly INamePlateGui _namePlateGui;
|
||||||
private readonly PairManager _pairManager;
|
private readonly PairManager _pairManager;
|
||||||
private readonly BroadcastService _broadcastService;
|
|
||||||
private readonly DalamudUtilService _dalamudUtil;
|
|
||||||
private readonly NameplateHandler _nameplatehandler;
|
|
||||||
private readonly IFramework _framework;
|
|
||||||
private readonly IGameGui _gameGui;
|
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<string, BroadcastEntry> _broadcastCache = new();
|
|
||||||
private static readonly TimeSpan MaxAllowedTtl = TimeSpan.FromMinutes(5);
|
|
||||||
private static readonly TimeSpan RetryDelay = TimeSpan.FromMinutes(1);
|
|
||||||
private readonly Queue<string> _lookupQueue = new();
|
|
||||||
private readonly HashSet<string> _lookupQueuedCids = new();
|
|
||||||
private readonly HashSet<string> _syncshellCids = new();
|
|
||||||
|
|
||||||
private readonly CancellationTokenSource _cleanupCts = new();
|
|
||||||
private Task? _cleanupTask;
|
|
||||||
|
|
||||||
private const int MaxLookupsPerFrame = 15;
|
|
||||||
private const int MaxQueueSize = 100;
|
|
||||||
|
|
||||||
private int _lookupsThisFrame = 0;
|
|
||||||
private int _frameCounter = 0;
|
|
||||||
|
|
||||||
public IReadOnlyDictionary<string, BroadcastEntry> BroadcastCache => _broadcastCache;
|
|
||||||
|
|
||||||
public readonly struct BroadcastEntry
|
|
||||||
{
|
|
||||||
public readonly bool IsBroadcasting;
|
|
||||||
public readonly DateTime ExpiryTime;
|
|
||||||
public readonly bool PrefixApplied;
|
|
||||||
public readonly string? GID;
|
|
||||||
|
|
||||||
public BroadcastEntry(bool isBroadcasting, DateTime expiryTime, bool prefixApplied, string? gid = null)
|
|
||||||
{
|
|
||||||
IsBroadcasting = isBroadcasting;
|
|
||||||
ExpiryTime = expiryTime;
|
|
||||||
PrefixApplied = prefixApplied;
|
|
||||||
GID = gid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public NameplateService(ILogger<NameplateService> logger,
|
public NameplateService(ILogger<NameplateService> logger,
|
||||||
LightlessConfigService configService,
|
LightlessConfigService configService,
|
||||||
INamePlateGui namePlateGui,
|
INamePlateGui namePlateGui,
|
||||||
IClientState clientState,
|
IClientState clientState,
|
||||||
PairManager pairManager,
|
PairManager pairManager,
|
||||||
BroadcastService broadcastService,
|
LightlessMediator lightlessMediator) : base(logger, lightlessMediator)
|
||||||
LightlessMediator lightlessMediator,
|
|
||||||
DalamudUtilService dalamudUtil,
|
|
||||||
NameplateHandler nameplatehandler,
|
|
||||||
IGameGui gameGui) : base(logger, lightlessMediator)
|
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_namePlateGui = namePlateGui;
|
_namePlateGui = namePlateGui;
|
||||||
_clientState = clientState;
|
_clientState = clientState;
|
||||||
_pairManager = pairManager;
|
_pairManager = pairManager;
|
||||||
_broadcastService = broadcastService;
|
|
||||||
_dalamudUtil = dalamudUtil;
|
|
||||||
_nameplatehandler = nameplatehandler;
|
|
||||||
_gameGui = gameGui;
|
|
||||||
|
|
||||||
_namePlateGui.OnNamePlateUpdate += OnNamePlateUpdate;
|
_namePlateGui.OnNamePlateUpdate += OnNamePlateUpdate;
|
||||||
_namePlateGui.RequestRedraw();
|
_namePlateGui.RequestRedraw();
|
||||||
Mediator.Subscribe<VisibilityChange>(this, (_) => _namePlateGui.RequestRedraw());
|
Mediator.Subscribe<VisibilityChange>(this, (_) => _namePlateGui.RequestRedraw());
|
||||||
Mediator.Subscribe<BroadcastStatusChangedMessage>(this, OnBroadcastStatusChanged);
|
|
||||||
|
|
||||||
_nameplatehandler.Init();
|
|
||||||
_cleanupTask = Task.Run(ExpiredBroadcastCleanupLoop);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
|
private void OnNamePlateUpdate(INamePlateUpdateContext context, IReadOnlyList<INamePlateUpdateHandler> handlers)
|
||||||
{
|
{
|
||||||
_frameCounter++;
|
|
||||||
_lookupsThisFrame = 0;
|
|
||||||
|
|
||||||
if (!_configService.Current.IsNameplateColorsEnabled || (_configService.Current.IsNameplateColorsEnabled && _clientState.IsPvPExcludingDen))
|
if (!_configService.Current.IsNameplateColorsEnabled || (_configService.Current.IsNameplateColorsEnabled && _clientState.IsPvPExcludingDen))
|
||||||
return;
|
return;
|
||||||
@@ -112,11 +58,6 @@ public class NameplateService : DisposableMediatorSubscriberBase
|
|||||||
if (playerCharacter == null)
|
if (playerCharacter == null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
|
||||||
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer(playerCharacter.Address);
|
|
||||||
var hasEntry = _broadcastCache.TryGetValue(cid, out var entry);
|
|
||||||
var isEntryStale = !hasEntry || entry.ExpiryTime <= now;
|
|
||||||
|
|
||||||
var isInParty = playerCharacter.StatusFlags.HasFlag(StatusFlags.PartyMember);
|
var isInParty = playerCharacter.StatusFlags.HasFlag(StatusFlags.PartyMember);
|
||||||
var isFriend = playerCharacter.StatusFlags.HasFlag(StatusFlags.Friend);
|
var isFriend = playerCharacter.StatusFlags.HasFlag(StatusFlags.Friend);
|
||||||
bool partyColorAllowed = (_configService.Current.overridePartyColor && isInParty);
|
bool partyColorAllowed = (_configService.Current.overridePartyColor && isInParty);
|
||||||
@@ -132,167 +73,7 @@ public class NameplateService : DisposableMediatorSubscriberBase
|
|||||||
handler.NameParts.TextWrap = CreateTextWrap(colors);
|
handler.NameParts.TextWrap = CreateTextWrap(colors);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_broadcastService.IsBroadcasting)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (isEntryStale && _lookupQueuedCids.Add(cid) && _lookupQueue.Count < MaxQueueSize)
|
|
||||||
{
|
|
||||||
_lookupQueue.Enqueue(cid);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_broadcastService.IsBroadcasting && _frameCounter % 2 == 0)
|
|
||||||
{
|
|
||||||
var cidsToLookup = new List<string>();
|
|
||||||
while (_lookupQueue.Count > 0 && _lookupsThisFrame < MaxLookupsPerFrame)
|
|
||||||
{
|
|
||||||
var nextCid = _lookupQueue.Dequeue();
|
|
||||||
_lookupQueuedCids.Remove(nextCid);
|
|
||||||
cidsToLookup.Add(nextCid);
|
|
||||||
_lookupsThisFrame++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cidsToLookup.Count > 0)
|
|
||||||
_ = BatchUpdateBroadcastCacheAsync(cidsToLookup);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task BatchUpdateBroadcastCacheAsync(List<string> cidList)
|
|
||||||
{
|
|
||||||
var results = await _broadcastService.AreUsersBroadcastingAsync(cidList).ConfigureAwait(false);
|
|
||||||
var now = DateTime.UtcNow;
|
|
||||||
|
|
||||||
foreach (var (cid, info) in results)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(cid) || info == null)
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Skipping broadcast entry: cid={Cid}, info=null or empty", cid);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool isBroadcasting = info.IsBroadcasting;
|
|
||||||
TimeSpan effectiveTtl = isBroadcasting && info.TTL.HasValue
|
|
||||||
? TimeSpan.FromTicks(Math.Min(info.TTL.Value.Ticks, MaxAllowedTtl.Ticks))
|
|
||||||
: RetryDelay;
|
|
||||||
|
|
||||||
var expiryTime = now + effectiveTtl;
|
|
||||||
|
|
||||||
_broadcastCache.AddOrUpdate(cid,
|
|
||||||
new BroadcastEntry(isBroadcasting, expiryTime, false, info.GID),
|
|
||||||
(_, old) => new BroadcastEntry(isBroadcasting, expiryTime, old.PrefixApplied, info.GID));
|
|
||||||
}
|
|
||||||
|
|
||||||
var activeCids = _broadcastCache
|
|
||||||
.Where(kvp => kvp.Value.IsBroadcasting)
|
|
||||||
.Select(kvp => kvp.Key)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
_nameplatehandler.UpdateBroadcastingCids(activeCids);
|
|
||||||
_namePlateGui.RequestRedraw();
|
|
||||||
|
|
||||||
UpdateSyncshellBroadcasts();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnBroadcastStatusChanged(BroadcastStatusChangedMessage msg)
|
|
||||||
{
|
|
||||||
if (!msg.Enabled)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Broadcast disabled, clearing prefix cache and queue");
|
|
||||||
|
|
||||||
_broadcastCache.Clear();
|
|
||||||
_lookupQueue.Clear();
|
|
||||||
_lookupQueuedCids.Clear();
|
|
||||||
_syncshellCids.Clear();
|
|
||||||
|
|
||||||
_nameplatehandler.UpdateBroadcastingCids(Enumerable.Empty<string>());
|
|
||||||
_namePlateGui.RequestRedraw();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<BroadcastStatusInfoDto> GetActiveSyncshellBroadcasts()
|
|
||||||
{
|
|
||||||
var now = DateTime.UtcNow;
|
|
||||||
|
|
||||||
return _broadcastCache
|
|
||||||
.Where(kvp =>
|
|
||||||
kvp.Value.IsBroadcasting &&
|
|
||||||
kvp.Value.ExpiryTime > now &&
|
|
||||||
!string.IsNullOrEmpty(kvp.Value.GID))
|
|
||||||
.Select(kvp => new BroadcastStatusInfoDto
|
|
||||||
{
|
|
||||||
HashedCID = kvp.Key,
|
|
||||||
IsBroadcasting = true,
|
|
||||||
TTL = kvp.Value.ExpiryTime - now,
|
|
||||||
GID = kvp.Value.GID
|
|
||||||
})
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateSyncshellBroadcasts()
|
|
||||||
{
|
|
||||||
var now = DateTime.UtcNow;
|
|
||||||
|
|
||||||
var newSet = _broadcastCache
|
|
||||||
.Where(kvp => kvp.Value.IsBroadcasting && kvp.Value.ExpiryTime > now && !string.IsNullOrEmpty(kvp.Value.GID))
|
|
||||||
.Select(kvp => kvp.Key)
|
|
||||||
.ToHashSet();
|
|
||||||
|
|
||||||
if (!_syncshellCids.SetEquals(newSet))
|
|
||||||
{
|
|
||||||
_syncshellCids.Clear();
|
|
||||||
foreach (var cid in newSet)
|
|
||||||
_syncshellCids.Add(cid);
|
|
||||||
|
|
||||||
_logger.LogInformation("Syncshell broadcast entries changed, sending update lol");
|
|
||||||
Mediator.Publish(new SyncshellBroadcastsUpdatedMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsBroadcastingKnown(string cidHash, out bool isBroadcasting)
|
|
||||||
{
|
|
||||||
if (_broadcastCache.TryGetValue(cidHash, out var entry))
|
|
||||||
{
|
|
||||||
isBroadcasting = entry.IsBroadcasting;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
isBroadcasting = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ExpiredBroadcastCleanupLoop()
|
|
||||||
{
|
|
||||||
var token = _cleanupCts.Token;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
while (!token.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
await Task.Delay(TimeSpan.FromSeconds(10), token);
|
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
|
||||||
foreach (var (cid, entry) in _broadcastCache.ToArray())
|
|
||||||
{
|
|
||||||
if (entry.ExpiryTime <= now)
|
|
||||||
{
|
|
||||||
if (_broadcastCache.TryRemove(cid, out _))
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Removed expired broadcast entry: {Cid}", cid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error in ExpiredBroadcastCleanupLoop");
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateSyncshellBroadcasts();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RequestRedraw()
|
public void RequestRedraw()
|
||||||
@@ -319,11 +100,7 @@ public class NameplateService : DisposableMediatorSubscriberBase
|
|||||||
{
|
{
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
|
|
||||||
_cleanupCts.Cancel();
|
|
||||||
_cleanupTask?.Wait(100);
|
|
||||||
|
|
||||||
_namePlateGui.OnNamePlateUpdate -= OnNamePlateUpdate;
|
_namePlateGui.OnNamePlateUpdate -= OnNamePlateUpdate;
|
||||||
_namePlateGui.RequestRedraw();
|
_namePlateGui.RequestRedraw();
|
||||||
_nameplatehandler.Uninit();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ namespace LightlessSync.UI
|
|||||||
private readonly LightlessConfigService _configService;
|
private readonly LightlessConfigService _configService;
|
||||||
private readonly BroadcastService _broadcastService;
|
private readonly BroadcastService _broadcastService;
|
||||||
private readonly UiSharedService _uiSharedService;
|
private readonly UiSharedService _uiSharedService;
|
||||||
private readonly NameplateService _nameplateService;
|
private readonly BroadcastScannerService _broadcastScannerService;
|
||||||
|
|
||||||
private IReadOnlyList<GroupFullInfoDto> _allSyncshells;
|
private IReadOnlyList<GroupFullInfoDto> _allSyncshells;
|
||||||
private string _userUid = string.Empty;
|
private string _userUid = string.Empty;
|
||||||
@@ -32,20 +32,20 @@ namespace LightlessSync.UI
|
|||||||
LightlessConfigService configService,
|
LightlessConfigService configService,
|
||||||
UiSharedService uiShared,
|
UiSharedService uiShared,
|
||||||
ApiController apiController,
|
ApiController apiController,
|
||||||
NameplateService nameplateService
|
BroadcastScannerService broadcastScannerService
|
||||||
) : base(logger, mediator, "Lightfinder###LightlessLightfinderUI", performanceCollectorService)
|
) : base(logger, mediator, "Lightfinder###LightlessLightfinderUI", performanceCollectorService)
|
||||||
{
|
{
|
||||||
_broadcastService = broadcastService;
|
_broadcastService = broadcastService;
|
||||||
_uiSharedService = uiShared;
|
_uiSharedService = uiShared;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_apiController = apiController;
|
_apiController = apiController;
|
||||||
_nameplateService = nameplateService;
|
_broadcastScannerService = broadcastScannerService;
|
||||||
|
|
||||||
IsOpen = false;
|
IsOpen = false;
|
||||||
this.SizeConstraints = new()
|
this.SizeConstraints = new()
|
||||||
{
|
{
|
||||||
MinimumSize = new(590, 340),
|
MinimumSize = new(600, 340),
|
||||||
MaximumSize = new(590, 340)
|
MaximumSize = new(750, 400)
|
||||||
};
|
};
|
||||||
|
|
||||||
mediator.Subscribe<RefreshUiMessage>(this, async _ => await RefreshSyncshells());
|
mediator.Subscribe<RefreshUiMessage>(this, async _ => await RefreshSyncshells());
|
||||||
@@ -126,9 +126,7 @@ namespace LightlessSync.UI
|
|||||||
{
|
{
|
||||||
if (!_broadcastService.IsLightFinderAvailable)
|
if (!_broadcastService.IsLightFinderAvailable)
|
||||||
{
|
{
|
||||||
ImGui.PushStyleColor(ImGuiCol.Text, UIColors.Get("LightlessYellow"));
|
_uiSharedService.MediumText("This server doesn't support Lightfinder.", UIColors.Get("LightlessYellow"));
|
||||||
ImGui.TextWrapped("This server doesn't support LightFinder.");
|
|
||||||
ImGui.PopStyleColor();
|
|
||||||
|
|
||||||
ImGuiHelpers.ScaledDummy(0.25f);
|
ImGuiHelpers.ScaledDummy(0.25f);
|
||||||
}
|
}
|
||||||
@@ -203,7 +201,7 @@ namespace LightlessSync.UI
|
|||||||
else
|
else
|
||||||
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("PairBlue"));
|
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("PairBlue"));
|
||||||
|
|
||||||
if (isOnCooldown)
|
if (isOnCooldown || !_broadcastService.IsLightFinderAvailable)
|
||||||
ImGui.BeginDisabled();
|
ImGui.BeginDisabled();
|
||||||
|
|
||||||
string buttonText = isBroadcasting ? "Disable Lightfinder" : "Enable Lightfinder";
|
string buttonText = isBroadcasting ? "Disable Lightfinder" : "Enable Lightfinder";
|
||||||
@@ -213,7 +211,7 @@ namespace LightlessSync.UI
|
|||||||
_broadcastService.ToggleBroadcast();
|
_broadcastService.ToggleBroadcast();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isOnCooldown)
|
if (isOnCooldown || !_broadcastService.IsLightFinderAvailable)
|
||||||
ImGui.EndDisabled();
|
ImGui.EndDisabled();
|
||||||
|
|
||||||
ImGui.PopStyleColor();
|
ImGui.PopStyleColor();
|
||||||
@@ -316,7 +314,7 @@ namespace LightlessSync.UI
|
|||||||
{
|
{
|
||||||
ImGui.Text("Broadcast Cache");
|
ImGui.Text("Broadcast Cache");
|
||||||
|
|
||||||
if (ImGui.BeginTable("##BroadcastCacheTable", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY, new Vector2(-1, 200f)))
|
if (ImGui.BeginTable("##BroadcastCacheTable", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY, new Vector2(-1, 225f)))
|
||||||
{
|
{
|
||||||
ImGui.TableSetupColumn("CID", ImGuiTableColumnFlags.WidthStretch);
|
ImGui.TableSetupColumn("CID", ImGuiTableColumnFlags.WidthStretch);
|
||||||
ImGui.TableSetupColumn("IsBroadcasting", ImGuiTableColumnFlags.WidthStretch);
|
ImGui.TableSetupColumn("IsBroadcasting", ImGuiTableColumnFlags.WidthStretch);
|
||||||
@@ -326,7 +324,7 @@ namespace LightlessSync.UI
|
|||||||
|
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
foreach (var (cid, entry) in _nameplateService.BroadcastCache)
|
foreach (var (cid, entry) in _broadcastScannerService.BroadcastCache)
|
||||||
{
|
{
|
||||||
ImGui.TableNextRow();
|
ImGui.TableNextRow();
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
private readonly LightlessConfigService _configService;
|
private readonly LightlessConfigService _configService;
|
||||||
private readonly BroadcastService _broadcastService;
|
private readonly BroadcastService _broadcastService;
|
||||||
private readonly UiSharedService _uiSharedService;
|
private readonly UiSharedService _uiSharedService;
|
||||||
private readonly NameplateService _nameplateService;
|
private readonly BroadcastScannerService _broadcastScannerService;
|
||||||
|
|
||||||
private readonly List<GroupJoinDto> _nearbySyncshells = new();
|
private readonly List<GroupJoinDto> _nearbySyncshells = new();
|
||||||
private int _selectedNearbyIndex = -1;
|
private int _selectedNearbyIndex = -1;
|
||||||
@@ -40,23 +40,24 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
LightlessConfigService configService,
|
LightlessConfigService configService,
|
||||||
UiSharedService uiShared,
|
UiSharedService uiShared,
|
||||||
ApiController apiController,
|
ApiController apiController,
|
||||||
NameplateService nameplateService
|
BroadcastScannerService broadcastScannerService
|
||||||
) : base(logger, mediator, "Shellfinder###LightlessSyncshellFinderUI", performanceCollectorService)
|
) : base(logger, mediator, "Shellfinder###LightlessSyncshellFinderUI", performanceCollectorService)
|
||||||
{
|
{
|
||||||
_broadcastService = broadcastService;
|
_broadcastService = broadcastService;
|
||||||
_uiSharedService = uiShared;
|
_uiSharedService = uiShared;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_apiController = apiController;
|
_apiController = apiController;
|
||||||
_nameplateService = nameplateService;
|
_broadcastScannerService = broadcastScannerService;
|
||||||
|
|
||||||
IsOpen = false;
|
IsOpen = false;
|
||||||
SizeConstraints = new()
|
SizeConstraints = new()
|
||||||
{
|
{
|
||||||
MinimumSize = new(600, 400),
|
MinimumSize = new(600, 400),
|
||||||
MaximumSize = new(600, 400)
|
MaximumSize = new(600, 550)
|
||||||
};
|
};
|
||||||
|
|
||||||
Mediator.Subscribe<SyncshellBroadcastsUpdatedMessage>(this, async _ => await RefreshSyncshellsAsync());
|
Mediator.Subscribe<SyncshellBroadcastsUpdatedMessage>(this, async _ => await RefreshSyncshellsAsync());
|
||||||
|
Mediator.Subscribe<BroadcastStatusChangedMessage>(this, async _ => await RefreshSyncshellsAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async void OnOpen()
|
public override async void OnOpen()
|
||||||
@@ -222,7 +223,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
|
|||||||
|
|
||||||
private async Task RefreshSyncshellsAsync()
|
private async Task RefreshSyncshellsAsync()
|
||||||
{
|
{
|
||||||
var syncshellBroadcasts = _nameplateService.GetActiveSyncshellBroadcasts();
|
var syncshellBroadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts();
|
||||||
|
|
||||||
if (syncshellBroadcasts.Count == 0)
|
if (syncshellBroadcasts.Count == 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ namespace LightlessSync.UI
|
|||||||
{ "LightlessPurpleDefault", "#9375d1" },
|
{ "LightlessPurpleDefault", "#9375d1" },
|
||||||
|
|
||||||
{ "ButtonDefault", "#323232" },
|
{ "ButtonDefault", "#323232" },
|
||||||
|
{ "FullBlack", "#000000" },
|
||||||
|
|
||||||
{ "LightlessBlue", "#a6c2ff" },
|
{ "LightlessBlue", "#a6c2ff" },
|
||||||
{ "LightlessYellow", "#ffe97a" },
|
{ "LightlessYellow", "#ffe97a" },
|
||||||
|
|||||||
Reference in New Issue
Block a user