233 lines
7.7 KiB
C#
233 lines
7.7 KiB
C#
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 _checkEveryFrames = 20;
|
|
private int _frameCounter = 0;
|
|
private int _lookupsThisFrame = 0;
|
|
private const int MaxLookupsPerFrame = 30;
|
|
private const int MaxQueueSize = 100;
|
|
|
|
private volatile bool _batchRunning = false;
|
|
|
|
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 % _checkEveryFrames == 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 && !_batchRunning)
|
|
{
|
|
_batchRunning = true;
|
|
_ = BatchUpdateBroadcastCacheAsync(cidsToLookup).ContinueWith(_ => _batchRunning = false);
|
|
}
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
public int CountActiveBroadcasts(string? excludeHashedCid = null)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
var comparer = StringComparer.Ordinal;
|
|
return _broadcastCache.Count(entry =>
|
|
entry.Value.IsBroadcasting &&
|
|
entry.Value.ExpiryTime > now &&
|
|
(excludeHashedCid is null || !comparer.Equals(entry.Key, excludeHashedCid)));
|
|
}
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
base.Dispose(disposing);
|
|
_framework.Update -= OnFrameworkUpdate;
|
|
_cleanupCts.Cancel();
|
|
_cleanupTask?.Wait(100);
|
|
_nameplateHandler.Uninit();
|
|
}
|
|
}
|