329 lines
11 KiB
C#
329 lines
11 KiB
C#
using Dalamud.Game.ClientState.Objects.Enums;
|
|
using Dalamud.Game.Gui.NamePlate;
|
|
using Dalamud.Game.Text.SeStringHandling;
|
|
using Dalamud.Plugin.Services;
|
|
using Dalamud.Utility;
|
|
using LightlessSync.API.Dto.User;
|
|
using LightlessSync.LightlessConfiguration;
|
|
using LightlessSync.PlayerData.Pairs;
|
|
using LightlessSync.Services.Mediator;
|
|
using LightlessSync.UI;
|
|
using Microsoft.Extensions.Logging;
|
|
using System.Collections.Concurrent;
|
|
|
|
namespace LightlessSync.Services;
|
|
|
|
public class NameplateService : DisposableMediatorSubscriberBase
|
|
{
|
|
private readonly ILogger<NameplateService> _logger;
|
|
private readonly LightlessConfigService _configService;
|
|
private readonly IClientState _clientState;
|
|
private readonly INamePlateGui _namePlateGui;
|
|
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,
|
|
LightlessConfigService configService,
|
|
INamePlateGui namePlateGui,
|
|
IClientState clientState,
|
|
PairManager pairManager,
|
|
BroadcastService broadcastService,
|
|
LightlessMediator lightlessMediator,
|
|
DalamudUtilService dalamudUtil,
|
|
NameplateHandler nameplatehandler,
|
|
IGameGui gameGui) : base(logger, lightlessMediator)
|
|
{
|
|
_logger = logger;
|
|
_configService = configService;
|
|
_namePlateGui = namePlateGui;
|
|
_clientState = clientState;
|
|
_pairManager = pairManager;
|
|
_broadcastService = broadcastService;
|
|
_dalamudUtil = dalamudUtil;
|
|
_nameplatehandler = nameplatehandler;
|
|
_gameGui = gameGui;
|
|
|
|
_namePlateGui.OnNamePlateUpdate += OnNamePlateUpdate;
|
|
_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)
|
|
{
|
|
_frameCounter++;
|
|
_lookupsThisFrame = 0;
|
|
|
|
if (!_configService.Current.IsNameplateColorsEnabled || (_configService.Current.IsNameplateColorsEnabled && _clientState.IsPvPExcludingDen))
|
|
return;
|
|
|
|
var visibleUsersIds = _pairManager.GetOnlineUserPairs()
|
|
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
|
|
.Select(u => (ulong)u.PlayerCharacterId)
|
|
.ToHashSet();
|
|
|
|
var now = DateTime.UtcNow;
|
|
var colors = _configService.Current.NameplateColors;
|
|
|
|
foreach (var handler in handlers)
|
|
{
|
|
var playerCharacter = handler.PlayerCharacter;
|
|
if (playerCharacter == null)
|
|
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 isFriend = playerCharacter.StatusFlags.HasFlag(StatusFlags.Friend);
|
|
bool partyColorAllowed = (_configService.Current.overridePartyColor && isInParty);
|
|
bool friendColorAllowed = (_configService.Current.overrideFriendColor && isFriend);
|
|
|
|
if (visibleUsersIds.Contains(handler.GameObjectId) &&
|
|
!(
|
|
(isInParty && !partyColorAllowed) ||
|
|
(isFriend && !friendColorAllowed)
|
|
))
|
|
{
|
|
//_logger.LogInformation("added nameplate color to {Name}", playerCharacter.Name.TextValue);
|
|
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()
|
|
{
|
|
|
|
_namePlateGui.RequestRedraw();
|
|
}
|
|
|
|
private static (SeString, SeString) CreateTextWrap(DtrEntry.Colors color)
|
|
{
|
|
var left = new Lumina.Text.SeStringBuilder();
|
|
var right = new Lumina.Text.SeStringBuilder();
|
|
|
|
left.PushColorRgba(color.Foreground);
|
|
right.PopColor();
|
|
|
|
left.PushEdgeColorRgba(color.Glow);
|
|
right.PopEdgeColor();
|
|
|
|
return (left.ToReadOnlySeString().ToDalamudString(), right.ToReadOnlySeString().ToDalamudString());
|
|
}
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
base.Dispose(disposing);
|
|
|
|
_cleanupCts.Cancel();
|
|
_cleanupTask?.Wait(100);
|
|
|
|
_namePlateGui.OnNamePlateUpdate -= OnNamePlateUpdate;
|
|
_namePlateGui.RequestRedraw();
|
|
_nameplatehandler.Uninit();
|
|
}
|
|
} |