using LightlessSync.API.Data.Extensions; using LightlessSync.API.Dto.User; using Microsoft.Extensions.Logging; namespace LightlessSync.PlayerData.Pairs; /// /// creates, tracks, and removes pair handlers /// public sealed class PairHandlerRegistry : IDisposable { private readonly object _gate = new(); private readonly Dictionary _entriesByIdent = new(StringComparer.Ordinal); private readonly Dictionary _entriesByHandler = new(); private readonly IPairHandlerAdapterFactory _handlerFactory; private readonly PairManager _pairManager; private readonly PairStateCache _pairStateCache; private readonly ILogger _logger; private readonly TimeSpan _deletionGracePeriod = TimeSpan.FromMinutes(5); public PairHandlerRegistry( IPairHandlerAdapterFactory handlerFactory, PairManager pairManager, PairStateCache pairStateCache, ILogger logger) { _handlerFactory = handlerFactory; _pairManager = pairManager; _pairStateCache = pairStateCache; _logger = logger; } public int GetVisibleUsersCount() { lock (_gate) { return _entriesByHandler.Keys.Count(handler => handler.IsVisible); } } public bool IsIdentVisible(string ident) { lock (_gate) { return _entriesByIdent.TryGetValue(ident, out var entry) && entry.Handler.IsVisible; } } public PairOperationResult RegisterOnlinePair(PairRegistration registration) { if (registration.CharacterIdent is null) { return PairOperationResult.Fail($"Registration for {registration.PairIdent.UserId} missing ident."); } IPairHandlerAdapter handler; lock (_gate) { var entry = GetOrCreateEntry(registration.CharacterIdent); handler = entry.Handler; handler.ScheduledForDeletion = false; entry.AddPair(registration.PairIdent); } ApplyPauseStateForHandler(handler); if (handler.LastReceivedCharacterData is null) { var cachedData = _pairStateCache.TryLoad(registration.CharacterIdent); if (cachedData is not null) { handler.LoadCachedCharacterData(cachedData); } } if (handler.LastReceivedCharacterData is not null && (handler.LastAppliedApproximateVRAMBytes < 0 || handler.LastAppliedDataTris < 0)) { handler.ApplyLastReceivedData(forced: true); } return PairOperationResult.Ok(registration.PairIdent); } public PairOperationResult DeregisterOfflinePair(PairRegistration registration, bool forceDisposal = false) { if (registration.CharacterIdent is null) { return PairOperationResult.Fail($"Deregister for {registration.PairIdent.UserId} missing ident."); } IPairHandlerAdapter? handler = null; bool shouldScheduleRemoval = false; bool shouldDisposeImmediately = false; lock (_gate) { if (!_entriesByIdent.TryGetValue(registration.CharacterIdent, out var entry)) { return PairOperationResult.Fail($"Ident {registration.CharacterIdent} not registered."); } handler = entry.Handler; entry.RemovePair(registration.PairIdent); if (entry.PairCount == 0) { if (forceDisposal) { shouldDisposeImmediately = true; } else { shouldScheduleRemoval = true; handler.ScheduledForDeletion = true; } } } if (shouldDisposeImmediately && handler is not null) { if (TryFinalizeHandlerRemoval(handler)) { handler.Dispose(); } } else if (shouldScheduleRemoval && handler is not null) { _ = RemoveAfterGracePeriodAsync(handler); } return PairOperationResult.Ok(registration.PairIdent); } public PairOperationResult ApplyCharacterData(PairRegistration registration, OnlineUserCharaDataDto dto) { if (registration.CharacterIdent is null) { return PairOperationResult.Fail($"Character data received without ident for {registration.PairIdent.UserId}."); } if (!TryGetHandler(registration.CharacterIdent, out var handler) || handler is null) { var registerResult = RegisterOnlinePair(registration); if (!registerResult.Success) { return PairOperationResult.Fail(registerResult.Error); } if (!TryGetHandler(registration.CharacterIdent, out handler) || handler is null) { return PairOperationResult.Fail($"Handler not ready for {registration.PairIdent.UserId}."); } } handler.ApplyData(dto.CharaData); return PairOperationResult.Ok(); } public PairOperationResult ApplyLastReceivedData(PairUniqueIdentifier pairIdent, string ident, bool forced = false) { if (!TryGetHandler(ident, out var handler) || handler is null) { return PairOperationResult.Fail($"Cannot reapply data: handler for {pairIdent.UserId} not found."); } handler.ApplyLastReceivedData(forced); return PairOperationResult.Ok(); } public PairOperationResult SetUploading(PairUniqueIdentifier pairIdent, string ident, bool uploading) { if (!TryGetHandler(ident, out var handler) || handler is null) { return PairOperationResult.Fail($"Cannot set uploading for {pairIdent.UserId}: handler not found."); } handler.SetUploading(uploading); return PairOperationResult.Ok(); } public PairOperationResult SetPausedState(PairUniqueIdentifier pairIdent, string ident, bool paused) { if (!TryGetHandler(ident, out var handler) || handler is null) { return PairOperationResult.Fail($"Cannot update pause state for {pairIdent.UserId}: handler not found."); } _ = paused; // value reflected in pair manager already ApplyPauseStateForHandler(handler); return PairOperationResult.Ok(); } public PairOperationResult> GetPairConnections(string ident) { PairHandlerEntry? entry; lock (_gate) { _entriesByIdent.TryGetValue(ident, out entry); } if (entry is null) { return PairOperationResult>.Fail($"No handler registered for {ident}."); } var list = new List<(PairUniqueIdentifier, PairConnection)>(); foreach (var pairIdent in entry.SnapshotPairs()) { var result = _pairManager.GetPair(pairIdent.UserId); if (result.Success) { list.Add((pairIdent, result.Value)); } } return PairOperationResult>.Ok(list); } private void ApplyPauseStateForHandler(IPairHandlerAdapter handler) { var pairs = _pairManager.GetPairsByIdent(handler.Ident); bool paused = pairs.Any(p => p.SelfToOtherPermissions.IsPaused() || p.OtherToSelfPermissions.IsPaused()); handler.SetPaused(paused); } internal bool TryGetHandler(string ident, out IPairHandlerAdapter? handler) { lock (_gate) { var success = _entriesByIdent.TryGetValue(ident, out var entry); handler = entry?.Handler; return success; } } internal IReadOnlyList GetHandlerSnapshot() { lock (_gate) { return _entriesByHandler.Keys.ToList(); } } internal IReadOnlyCollection GetRegisteredPairs(IPairHandlerAdapter handler) { lock (_gate) { if (_entriesByHandler.TryGetValue(handler, out var entry)) { return entry.SnapshotPairs(); } } return Array.Empty(); } internal void ReapplyAll(bool forced = false) { var handlers = GetHandlerSnapshot(); foreach (var handler in handlers) { try { handler.ApplyLastReceivedData(forced); } catch (Exception ex) { if (_logger.IsEnabled(LogLevel.Debug)) { _logger.LogDebug(ex, "Failed to reapply cached data for {Ident}", handler.Ident); } } } } internal void ResetAllHandlers() { List handlers; lock (_gate) { handlers = _entriesByHandler.Keys.ToList(); _entriesByIdent.Clear(); _entriesByHandler.Clear(); } foreach (var handler in handlers) { try { handler.Dispose(); } catch (Exception ex) { if (_logger.IsEnabled(LogLevel.Debug)) { _logger.LogDebug(ex, "Failed to dispose handler for {Ident}", handler.Ident); } } } } public void Dispose() { List handlers; lock (_gate) { handlers = _entriesByHandler.Keys.ToList(); _entriesByIdent.Clear(); _entriesByHandler.Clear(); } foreach (var handler in handlers) { handler.Dispose(); } } private PairHandlerEntry GetOrCreateEntry(string ident) { if (_entriesByIdent.TryGetValue(ident, out var entry)) { return entry; } var handler = _handlerFactory.Create(ident); entry = new PairHandlerEntry(ident, handler); _entriesByIdent[ident] = entry; _entriesByHandler[handler] = entry; return entry; } private async Task RemoveAfterGracePeriodAsync(IPairHandlerAdapter handler) { await Task.Delay(_deletionGracePeriod).ConfigureAwait(false); if (TryFinalizeHandlerRemoval(handler)) { handler.Dispose(); } } private bool TryFinalizeHandlerRemoval(IPairHandlerAdapter handler) { lock (_gate) { if (!_entriesByHandler.TryGetValue(handler, out var entry) || entry.HasPairs) { handler.ScheduledForDeletion = false; return false; } _entriesByHandler.Remove(handler); _entriesByIdent.Remove(entry.Ident); return true; } } }