test-abel-cake-changes

This commit is contained in:
cake
2026-01-03 22:45:55 +01:00
57 changed files with 11420 additions and 357 deletions

View File

@@ -1,6 +1,7 @@
using LightlessSync.FileCache;
using LightlessSync.LightlessConfiguration;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ModelDecimation;
using LightlessSync.Services.TextureCompression;
using LightlessSync.WebAPI.Files;
using Microsoft.Extensions.Logging;
@@ -16,6 +17,7 @@ public class FileDownloadManagerFactory
private readonly FileCompactor _fileCompactor;
private readonly LightlessConfigService _configService;
private readonly TextureDownscaleService _textureDownscaleService;
private readonly ModelDecimationService _modelDecimationService;
private readonly TextureMetadataHelper _textureMetadataHelper;
public FileDownloadManagerFactory(
@@ -26,6 +28,7 @@ public class FileDownloadManagerFactory
FileCompactor fileCompactor,
LightlessConfigService configService,
TextureDownscaleService textureDownscaleService,
ModelDecimationService modelDecimationService,
TextureMetadataHelper textureMetadataHelper)
{
_loggerFactory = loggerFactory;
@@ -35,6 +38,7 @@ public class FileDownloadManagerFactory
_fileCompactor = fileCompactor;
_configService = configService;
_textureDownscaleService = textureDownscaleService;
_modelDecimationService = modelDecimationService;
_textureMetadataHelper = textureMetadataHelper;
}
@@ -48,6 +52,7 @@ public class FileDownloadManagerFactory
_fileCompactor,
_configService,
_textureDownscaleService,
_modelDecimationService,
_textureMetadataHelper);
}
}

View File

@@ -16,4 +16,5 @@ public interface IPairPerformanceSubject
long LastAppliedApproximateVRAMBytes { get; set; }
long LastAppliedApproximateEffectiveVRAMBytes { get; set; }
long LastAppliedDataTris { get; set; }
long LastAppliedApproximateEffectiveTris { get; set; }
}

View File

@@ -69,6 +69,7 @@ public class Pair
public string? PlayerName => TryGetHandler()?.PlayerName ?? UserPair.User.AliasOrUID;
public long LastAppliedDataBytes => TryGetHandler()?.LastAppliedDataBytes ?? -1;
public long LastAppliedDataTris => TryGetHandler()?.LastAppliedDataTris ?? -1;
public long LastAppliedApproximateEffectiveTris => TryGetHandler()?.LastAppliedApproximateEffectiveTris ?? -1;
public long LastAppliedApproximateVRAMBytes => TryGetHandler()?.LastAppliedApproximateVRAMBytes ?? -1;
public long LastAppliedApproximateEffectiveVRAMBytes => TryGetHandler()?.LastAppliedApproximateEffectiveVRAMBytes ?? -1;
public string Ident => TryGetHandler()?.Ident ?? TryGetConnection()?.Ident ?? string.Empty;

View File

@@ -125,6 +125,7 @@ public sealed partial class PairCoordinator
}
}
_mediator.Publish(new PairOnlineMessage(new PairUniqueIdentifier(dto.User.UID)));
PublishPairDataChanged();
}

View File

@@ -1,5 +1,6 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using Dalamud.Plugin.Services;
using LightlessSync.API.Data;
using LightlessSync.API.Data.Enum;
using LightlessSync.API.Data.Extensions;
@@ -11,6 +12,7 @@ using LightlessSync.Services;
using LightlessSync.Services.ActorTracking;
using LightlessSync.Services.Events;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ModelDecimation;
using LightlessSync.Services.PairProcessing;
using LightlessSync.Services.ServerConfiguration;
using LightlessSync.Services.TextureCompression;
@@ -45,12 +47,14 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private readonly ServerConfigurationManager _serverConfigManager;
private readonly PluginWarningNotificationService _pluginWarningNotificationManager;
private readonly TextureDownscaleService _textureDownscaleService;
private readonly ModelDecimationService _modelDecimationService;
private readonly PairStateCache _pairStateCache;
private readonly PairPerformanceMetricsCache _performanceMetricsCache;
private readonly XivDataAnalyzer _modelAnalyzer;
private readonly PenumbraTempCollectionJanitor _tempCollectionJanitor;
private readonly LightlessConfigService _configService;
private readonly PairManager _pairManager;
private readonly IFramework _framework;
private CancellationTokenSource? _applicationCancellationTokenSource;
private Guid _applicationId;
private Task? _applicationTask;
@@ -69,6 +73,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private int _lastMissingNonCriticalMods;
private int _lastMissingForbiddenMods;
private bool _lastMissingCachedFiles;
private string? _lastSuccessfulDataHash;
private bool _isVisible;
private Guid _penumbraCollection;
private readonly object _collectionGate = new();
@@ -85,6 +90,13 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private readonly object _visibilityGraceGate = new();
private CancellationTokenSource? _visibilityGraceCts;
private static readonly TimeSpan VisibilityEvictionGrace = TimeSpan.FromMinutes(1);
private readonly object _ownedRetryGate = new();
private readonly Dictionary<ObjectKind, HashSet<PlayerChanges>> _pendingOwnedChanges = new();
private CancellationTokenSource? _ownedRetryCts;
private Task _ownedRetryTask = Task.CompletedTask;
private static readonly TimeSpan OwnedRetryInitialDelay = TimeSpan.FromSeconds(1);
private static readonly TimeSpan OwnedRetryMaxDelay = TimeSpan.FromSeconds(10);
private static readonly TimeSpan OwnedRetryStaleDataGrace = TimeSpan.FromMinutes(5);
private static readonly HashSet<string> NonPriorityModExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".tmb",
@@ -102,10 +114,15 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private DateTime _nextActorLookupUtc = DateTime.MinValue;
private static readonly TimeSpan ActorLookupInterval = TimeSpan.FromSeconds(1);
private static readonly SemaphoreSlim ActorInitializationLimiter = new(1, 1);
private const int FullyLoadedTimeoutMsPlayer = 30000;
private const int FullyLoadedTimeoutMsOther = 5000;
private readonly object _actorInitializationGate = new();
private ActorObjectService.ActorDescriptor? _pendingActorDescriptor;
private bool _actorInitializationInProgress;
private bool _frameworkUpdateSubscribed;
private nint _lastKnownAddress = nint.Zero;
private ushort _lastKnownObjectIndex = ushort.MaxValue;
private string? _lastKnownName;
public DateTime? InvisibleSinceUtc => _invisibleSinceUtc;
public DateTime? VisibilityEvictionDueAtUtc => _visibilityEvictionDueAtUtc;
@@ -154,6 +171,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
public long LastAppliedDataBytes { get; private set; }
public long LastAppliedDataTris { get; set; } = -1;
public long LastAppliedApproximateEffectiveTris { get; set; } = -1;
public long LastAppliedApproximateVRAMBytes { get; set; } = -1;
public long LastAppliedApproximateEffectiveVRAMBytes { get; set; } = -1;
public CharacterData? LastReceivedCharacterData { get; private set; }
@@ -182,6 +200,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
FileDownloadManager transferManager,
PluginWarningNotificationService pluginWarningNotificationManager,
DalamudUtilService dalamudUtil,
IFramework framework,
ActorObjectService actorObjectService,
IHostApplicationLifetime lifetime,
FileCacheManager fileDbManager,
@@ -189,6 +208,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
PairProcessingLimiter pairProcessingLimiter,
ServerConfigurationManager serverConfigManager,
TextureDownscaleService textureDownscaleService,
ModelDecimationService modelDecimationService,
PairStateCache pairStateCache,
PairPerformanceMetricsCache performanceMetricsCache,
PenumbraTempCollectionJanitor tempCollectionJanitor,
@@ -202,6 +222,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
_downloadManager = transferManager;
_pluginWarningNotificationManager = pluginWarningNotificationManager;
_dalamudUtil = dalamudUtil;
_framework = framework;
_actorObjectService = actorObjectService;
_lifetime = lifetime;
_fileDbManager = fileDbManager;
@@ -209,6 +230,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
_pairProcessingLimiter = pairProcessingLimiter;
_serverConfigManager = serverConfigManager;
_textureDownscaleService = textureDownscaleService;
_modelDecimationService = modelDecimationService;
_pairStateCache = pairStateCache;
_performanceMetricsCache = performanceMetricsCache;
_tempCollectionJanitor = tempCollectionJanitor;
@@ -236,7 +258,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
return;
}
if (LastAppliedDataBytes < 0 || LastAppliedDataTris < 0
if (LastAppliedDataBytes < 0 || LastAppliedDataTris < 0 || LastAppliedApproximateEffectiveTris < 0
|| LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0)
{
_forceApplyMods = true;
@@ -443,7 +465,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
}
}
private void ResetPenumbraCollection(bool releaseFromPenumbra = true, string? reason = null)
private void ResetPenumbraCollection(bool releaseFromPenumbra = true, string? reason = null, bool awaitIpc = true)
{
Guid toRelease = Guid.Empty;
bool hadCollection = false;
@@ -477,16 +499,33 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
return;
}
try
var applicationId = Guid.NewGuid();
if (awaitIpc)
{
var applicationId = Guid.NewGuid();
Logger.LogTrace("[{applicationId}] Removing temp collection {CollectionId} for {handler} ({reason})", applicationId, toRelease, GetLogIdentifier(), reason ?? "Cleanup");
_ipcManager.Penumbra.RemoveTemporaryCollectionAsync(Logger, applicationId, toRelease).GetAwaiter().GetResult();
try
{
Logger.LogTrace("[{applicationId}] Removing temp collection {CollectionId} for {handler} ({reason})", applicationId, toRelease, GetLogIdentifier(), reason ?? "Cleanup");
_ipcManager.Penumbra.RemoveTemporaryCollectionAsync(Logger, applicationId, toRelease).GetAwaiter().GetResult();
}
catch (Exception ex)
{
Logger.LogDebug(ex, "Failed to remove temporary Penumbra collection for {handler}", GetLogIdentifier());
}
return;
}
catch (Exception ex)
_ = Task.Run(async () =>
{
Logger.LogDebug(ex, "Failed to remove temporary Penumbra collection for {handler}", GetLogIdentifier());
}
try
{
Logger.LogTrace("[{applicationId}] Removing temp collection {CollectionId} for {handler} ({reason})", applicationId, toRelease, GetLogIdentifier(), reason ?? "Cleanup");
await _ipcManager.Penumbra.RemoveTemporaryCollectionAsync(Logger, applicationId, toRelease).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.LogDebug(ex, "Failed to remove temporary Penumbra collection for {handler}", GetLogIdentifier());
}
});
}
private bool AnyPair(Func<PairConnection, bool> predicate)
@@ -556,6 +595,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
_forceApplyMods = true;
LastAppliedDataBytes = -1;
LastAppliedDataTris = -1;
LastAppliedApproximateEffectiveTris = -1;
LastAppliedApproximateVRAMBytes = -1;
LastAppliedApproximateEffectiveVRAMBytes = -1;
}
@@ -570,9 +610,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
}
var hasMissingCachedFiles = HasMissingCachedFiles(LastReceivedCharacterData);
var missingStarted = !_lastMissingCachedFiles && hasMissingCachedFiles;
var missingResolved = _lastMissingCachedFiles && !hasMissingCachedFiles;
_lastMissingCachedFiles = hasMissingCachedFiles;
var shouldForce = forced || missingResolved;
var shouldForce = forced || missingStarted || missingResolved;
var forceApplyCustomization = forced;
if (IsPaused())
{
@@ -580,25 +622,47 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
return;
}
if (shouldForce)
{
_forceApplyMods = true;
_forceFullReapply = true;
LastAppliedDataBytes = -1;
LastAppliedDataTris = -1;
LastAppliedApproximateVRAMBytes = -1;
LastAppliedApproximateEffectiveVRAMBytes = -1;
}
var sanitized = CloneAndSanitizeLastReceived(out _);
var sanitized = CloneAndSanitizeLastReceived(out var dataHash);
if (sanitized is null)
{
Logger.LogTrace("Sanitized data null for {Ident}", Ident);
return;
}
var dataApplied = !string.IsNullOrEmpty(dataHash)
&& string.Equals(dataHash, _lastSuccessfulDataHash ?? string.Empty, StringComparison.Ordinal);
var needsApply = !dataApplied;
var hasModReplacements = sanitized.FileReplacements.Values.Any(list => list.Count > 0);
var needsModReapply = needsApply && hasModReplacements;
var shouldForceMods = shouldForce || needsModReapply;
forceApplyCustomization = forced || needsApply;
var suppressForcedModRedraw = !forced && hasMissingCachedFiles && dataApplied;
if (shouldForceMods)
{
_forceApplyMods = true;
_forceFullReapply = true;
LastAppliedDataBytes = -1;
LastAppliedDataTris = -1;
LastAppliedApproximateEffectiveTris = -1;
LastAppliedApproximateVRAMBytes = -1;
LastAppliedApproximateEffectiveVRAMBytes = -1;
}
_pairStateCache.Store(Ident, sanitized);
if (!IsVisible && !_pauseRequested)
{
if (_charaHandler is not null && _charaHandler.Address == nint.Zero)
{
_charaHandler.Refresh();
}
if (PlayerCharacter != nint.Zero)
{
IsVisible = true;
}
}
if (!IsVisible)
{
Logger.LogTrace("Handler for {Ident} not visible, caching sanitized data for later", Ident);
@@ -607,7 +671,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
return;
}
ApplyCharacterData(Guid.NewGuid(), sanitized, shouldForce);
ApplyCharacterData(Guid.NewGuid(), sanitized, forceApplyCustomization, suppressForcedModRedraw);
}
public bool FetchPerformanceMetricsFromCache()
@@ -679,6 +743,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private void ApplyCachedMetrics(PairPerformanceMetrics metrics)
{
LastAppliedDataTris = metrics.TriangleCount;
LastAppliedApproximateEffectiveTris = metrics.ApproximateEffectiveTris;
LastAppliedApproximateVRAMBytes = metrics.ApproximateVramBytes;
LastAppliedApproximateEffectiveVRAMBytes = metrics.ApproximateEffectiveVramBytes;
}
@@ -686,6 +751,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private void StorePerformanceMetrics(CharacterData charaData)
{
if (LastAppliedDataTris < 0
|| LastAppliedApproximateEffectiveTris < 0
|| LastAppliedApproximateVRAMBytes < 0
|| LastAppliedApproximateEffectiveVRAMBytes < 0)
{
@@ -701,7 +767,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
_performanceMetricsCache.StoreMetrics(
Ident,
dataHash,
new PairPerformanceMetrics(LastAppliedDataTris, LastAppliedApproximateVRAMBytes, LastAppliedApproximateEffectiveVRAMBytes));
new PairPerformanceMetrics(LastAppliedDataTris, LastAppliedApproximateVRAMBytes, LastAppliedApproximateEffectiveVRAMBytes, LastAppliedApproximateEffectiveTris));
}
private bool HasMissingCachedFiles(CharacterData characterData)
@@ -917,7 +983,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
SetUploading(false);
}
public void ApplyCharacterData(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization = false)
public void ApplyCharacterData(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization = false, bool suppressForcedModRedraw = false)
{
_lastApplyAttemptAt = DateTime.UtcNow;
ClearFailureState();
@@ -1011,7 +1077,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Informational,
"Applying Character Data")));
var charaDataToUpdate = characterData.CheckUpdatedData(applicationBase, _cachedData?.DeepClone() ?? new(), Logger, this, forceApplyCustomization, _forceApplyMods);
var charaDataToUpdate = characterData.CheckUpdatedData(applicationBase, _cachedData?.DeepClone() ?? new(), Logger, this,
forceApplyCustomization, _forceApplyMods, suppressForcedModRedraw);
if (handlerReady && _forceApplyMods)
{
@@ -1032,7 +1099,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
Logger.LogDebug("[BASE-{appbase}] Downloading and applying character for {name}", applicationBase, GetPrimaryAliasOrUidSafe());
var forceFullReapply = _forceFullReapply
|| LastAppliedApproximateVRAMBytes < 0 || LastAppliedDataTris < 0;
|| LastAppliedApproximateVRAMBytes < 0 || LastAppliedDataTris < 0 || LastAppliedApproximateEffectiveTris < 0;
DownloadAndApplyCharacter(applicationBase, characterData.DeepClone(), charaDataToUpdate, forceFullReapply);
}
@@ -1108,12 +1175,183 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
}, CancellationToken.None);
}
private void ScheduleOwnedObjectRetry(ObjectKind kind, HashSet<PlayerChanges> changes)
{
if (kind == ObjectKind.Player || changes.Count == 0)
{
return;
}
lock (_ownedRetryGate)
{
_pendingOwnedChanges[kind] = new HashSet<PlayerChanges>(changes);
if (!_ownedRetryTask.IsCompleted)
{
return;
}
_ownedRetryCts = _ownedRetryCts?.CancelRecreate() ?? new CancellationTokenSource();
var token = _ownedRetryCts.Token;
_ownedRetryTask = Task.Run(() => OwnedObjectRetryLoopAsync(token), CancellationToken.None);
}
}
private void ClearOwnedObjectRetry(ObjectKind kind)
{
lock (_ownedRetryGate)
{
if (!_pendingOwnedChanges.Remove(kind))
{
return;
}
}
}
private void ClearAllOwnedObjectRetries()
{
lock (_ownedRetryGate)
{
_pendingOwnedChanges.Clear();
}
}
private bool IsOwnedRetryDataStale()
{
if (!_lastDataReceivedAt.HasValue)
{
return true;
}
return DateTime.UtcNow - _lastDataReceivedAt.Value > OwnedRetryStaleDataGrace;
}
private async Task OwnedObjectRetryLoopAsync(CancellationToken token)
{
var delay = OwnedRetryInitialDelay;
try
{
while (!token.IsCancellationRequested)
{
if (IsOwnedRetryDataStale())
{
ClearAllOwnedObjectRetries();
return;
}
Dictionary<ObjectKind, HashSet<PlayerChanges>> pending;
lock (_ownedRetryGate)
{
if (_pendingOwnedChanges.Count == 0)
{
return;
}
pending = _pendingOwnedChanges.ToDictionary(kvp => kvp.Key, kvp => new HashSet<PlayerChanges>(kvp.Value));
}
if (!IsVisible || IsPaused() || !CanApplyNow() || PlayerCharacter == nint.Zero || _charaHandler is null)
{
await Task.Delay(delay, token).ConfigureAwait(false);
delay = IncreaseRetryDelay(delay);
continue;
}
if ((_applicationTask?.IsCompleted ?? true) == false || (_pairDownloadTask?.IsCompleted ?? true) == false)
{
await Task.Delay(delay, token).ConfigureAwait(false);
delay = IncreaseRetryDelay(delay);
continue;
}
var sanitized = CloneAndSanitizeLastReceived(out _);
if (sanitized is null)
{
await Task.Delay(delay, token).ConfigureAwait(false);
delay = IncreaseRetryDelay(delay);
continue;
}
bool anyApplied = false;
foreach (var entry in pending)
{
if (!HasAppearanceDataForKind(sanitized, entry.Key))
{
ClearOwnedObjectRetry(entry.Key);
continue;
}
var applied = await ApplyCustomizationDataAsync(Guid.NewGuid(), entry, sanitized, token).ConfigureAwait(false);
if (applied)
{
ClearOwnedObjectRetry(entry.Key);
anyApplied = true;
}
}
if (!anyApplied)
{
await Task.Delay(delay, token).ConfigureAwait(false);
delay = IncreaseRetryDelay(delay);
}
else
{
delay = OwnedRetryInitialDelay;
}
}
}
catch (OperationCanceledException)
{
// ignore
}
catch (Exception ex)
{
Logger.LogDebug(ex, "Owned object retry task failed for {handler}", GetLogIdentifier());
}
}
private static TimeSpan IncreaseRetryDelay(TimeSpan delay)
{
var nextMs = Math.Min(delay.TotalMilliseconds * 2, OwnedRetryMaxDelay.TotalMilliseconds);
return TimeSpan.FromMilliseconds(nextMs);
}
private static bool HasAppearanceDataForKind(CharacterData data, ObjectKind kind)
{
if (data.FileReplacements.TryGetValue(kind, out var replacements) && replacements.Count > 0)
{
return true;
}
if (data.GlamourerData.TryGetValue(kind, out var glamourer) && !string.IsNullOrEmpty(glamourer))
{
return true;
}
if (data.CustomizePlusData.TryGetValue(kind, out var customize) && !string.IsNullOrEmpty(customize))
{
return true;
}
return false;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
SetUploading(false);
var name = PlayerName;
if (!string.IsNullOrEmpty(name))
{
_lastKnownName = name;
}
var currentAddress = PlayerCharacter;
if (currentAddress != nint.Zero)
{
_lastKnownAddress = currentAddress;
}
var user = GetPrimaryUserDataSafe();
var alias = GetPrimaryAliasOrUidSafe();
Logger.LogDebug("Disposing {name} ({user})", name, alias);
@@ -1124,6 +1362,9 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
_applicationCancellationTokenSource = null;
_downloadCancellationTokenSource?.CancelDispose();
_downloadCancellationTokenSource = null;
ClearAllOwnedObjectRetries();
_ownedRetryCts?.CancelDispose();
_ownedRetryCts = null;
_downloadManager.Dispose();
_charaHandler?.Dispose();
CancelVisibilityGraceTask();
@@ -1136,43 +1377,62 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
Mediator.Publish(new EventMessage(new Event(name, user, nameof(PairHandlerAdapter), EventSeverity.Informational, "Disposing User")));
}
if (_lifetime.ApplicationStopping.IsCancellationRequested) return;
if (_dalamudUtil is { IsZoning: false, IsInCutscene: false } && !string.IsNullOrEmpty(name))
if (IsFrameworkUnloading())
{
Logger.LogTrace("[{applicationId}] Restoring state for {name} ({user})", applicationId, name, alias);
Logger.LogDebug("[{applicationId}] Removing Temp Collection for {name} ({user})", applicationId, name, alias);
ResetPenumbraCollection(reason: nameof(Dispose));
if (!IsVisible)
Logger.LogWarning("Framework is unloading, skipping disposal for {name} ({user})", name, alias);
return;
}
var isStopping = _lifetime.ApplicationStopping.IsCancellationRequested;
if (isStopping)
{
ResetPenumbraCollection(reason: "DisposeStopping", awaitIpc: false);
ScheduleSafeRevertOnDisposal(applicationId, name, alias);
return;
}
var canCleanup = !string.IsNullOrEmpty(name)
&& _dalamudUtil.IsLoggedIn
&& !_dalamudUtil.IsZoning
&& !_dalamudUtil.IsInCutscene;
if (!canCleanup)
{
return;
}
Logger.LogTrace("[{applicationId}] Restoring state for {name} ({user})", applicationId, name, alias);
Logger.LogDebug("[{applicationId}] Removing Temp Collection for {name} ({user})", applicationId, name, alias);
ResetPenumbraCollection(reason: nameof(Dispose));
if (!IsVisible)
{
Logger.LogDebug("[{applicationId}] Restoring Glamourer for {name} ({user})", applicationId, name, alias);
_ipcManager.Glamourer.RevertByNameAsync(Logger, name, applicationId).GetAwaiter().GetResult();
}
else
{
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(60));
var effectiveCachedData = _cachedData ?? _pairStateCache.TryLoad(Ident);
if (effectiveCachedData is not null)
{
Logger.LogDebug("[{applicationId}] Restoring Glamourer for {name} ({user})", applicationId, name, alias);
_ipcManager.Glamourer.RevertByNameAsync(Logger, name, applicationId).GetAwaiter().GetResult();
_cachedData = effectiveCachedData;
}
else
Logger.LogInformation("[{applicationId}] CachedData is null {isNull}, contains things: {contains}",
applicationId, _cachedData == null, _cachedData?.FileReplacements.Any() ?? false);
foreach (KeyValuePair<ObjectKind, List<FileReplacementData>> item in _cachedData?.FileReplacements ?? [])
{
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(60));
var effectiveCachedData = _cachedData ?? _pairStateCache.TryLoad(Ident);
if (effectiveCachedData is not null)
try
{
_cachedData = effectiveCachedData;
RevertCustomizationDataAsync(item.Key, name, applicationId, cts.Token).GetAwaiter().GetResult();
}
Logger.LogInformation("[{applicationId}] CachedData is null {isNull}, contains things: {contains}",
applicationId, _cachedData == null, _cachedData?.FileReplacements.Any() ?? false);
foreach (KeyValuePair<ObjectKind, List<FileReplacementData>> item in _cachedData?.FileReplacements ?? [])
catch (InvalidOperationException ex)
{
try
{
RevertCustomizationDataAsync(item.Key, name, applicationId, cts.Token).GetAwaiter().GetResult();
}
catch (InvalidOperationException ex)
{
Logger.LogWarning(ex, "Failed disposing player (not present anymore?)");
break;
}
Logger.LogWarning(ex, "Failed disposing player (not present anymore?)");
break;
}
}
}
@@ -1185,6 +1445,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
{
PlayerName = null;
_cachedData = null;
_lastSuccessfulDataHash = null;
_lastAppliedModdedPaths = null;
_needsCollectionRebuild = false;
_performanceMetricsCache.Clear(Ident);
@@ -1192,9 +1453,145 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
}
}
private async Task ApplyCustomizationDataAsync(Guid applicationId, KeyValuePair<ObjectKind, HashSet<PlayerChanges>> changes, CharacterData charaData, CancellationToken token)
private bool IsFrameworkUnloading()
{
if (PlayerCharacter == nint.Zero) return;
try
{
var prop = _framework.GetType().GetProperty("IsFrameworkUnloading");
if (prop?.PropertyType == typeof(bool))
{
return (bool)prop.GetValue(_framework)!;
}
}
catch
{
// ignore
}
return false;
}
private void ScheduleSafeRevertOnDisposal(Guid applicationId, string? name, string alias)
{
var cleanupName = !string.IsNullOrEmpty(name) ? name : _lastKnownName;
var cleanupAddress = _lastKnownAddress != nint.Zero
? _lastKnownAddress
: _dalamudUtil.GetPlayerCharacterFromCachedTableByIdent(Ident);
var cleanupObjectIndex = _lastKnownObjectIndex;
var cleanupIdent = Ident;
var customizeIds = _customizeIds.Values.Where(id => id.HasValue)
.Select(id => id!.Value)
.Distinct()
.ToList();
if (string.IsNullOrEmpty(cleanupName)
&& cleanupAddress == nint.Zero
&& cleanupObjectIndex == ushort.MaxValue
&& customizeIds.Count == 0)
{
return;
}
_ = Task.Run(() => SafeRevertOnDisposalAsync(
applicationId,
cleanupName,
cleanupAddress,
cleanupObjectIndex,
cleanupIdent,
customizeIds,
alias));
}
private async Task SafeRevertOnDisposalAsync(
Guid applicationId,
string? cleanupName,
nint cleanupAddress,
ushort cleanupObjectIndex,
string cleanupIdent,
IReadOnlyList<Guid> customizeIds,
string alias)
{
try
{
if (IsFrameworkUnloading())
{
return;
}
if (!string.IsNullOrEmpty(cleanupName) && _ipcManager.Glamourer.APIAvailable)
{
Logger.LogDebug("[{applicationId}] Restoring Glamourer for {name} ({user})", applicationId, cleanupName, alias);
await _ipcManager.Glamourer.RevertByNameAsync(Logger, cleanupName, applicationId).ConfigureAwait(false);
}
if (_ipcManager.CustomizePlus.APIAvailable && customizeIds.Count > 0)
{
foreach (var customizeId in customizeIds)
{
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
}
}
var address = cleanupAddress;
if (address == nint.Zero && cleanupObjectIndex != ushort.MaxValue)
{
address = await _dalamudUtil.RunOnFrameworkThread(() =>
{
var obj = _dalamudUtil.GetCharacterFromObjectTableByIndex(cleanupObjectIndex);
if (obj is not Dalamud.Game.ClientState.Objects.SubKinds.IPlayerCharacter player)
{
return nint.Zero;
}
if (!DalamudUtilService.TryGetHashedCID(player, out var hash)
|| !string.Equals(hash, cleanupIdent, StringComparison.Ordinal))
{
return nint.Zero;
}
return player.Address;
}).ConfigureAwait(false);
}
if (address == nint.Zero)
{
return;
}
if (_ipcManager.CustomizePlus.APIAvailable)
{
await _ipcManager.CustomizePlus.RevertAsync(address).ConfigureAwait(false);
}
if (_ipcManager.Heels.APIAvailable)
{
await _ipcManager.Heels.RestoreOffsetForPlayerAsync(address).ConfigureAwait(false);
}
if (_ipcManager.Honorific.APIAvailable)
{
await _ipcManager.Honorific.ClearTitleAsync(address).ConfigureAwait(false);
}
if (_ipcManager.Moodles.APIAvailable)
{
await _ipcManager.Moodles.RevertStatusAsync(address).ConfigureAwait(false);
}
if (_ipcManager.PetNames.APIAvailable)
{
await _ipcManager.PetNames.ClearPlayerData(address).ConfigureAwait(false);
}
}
catch (Exception ex)
{
Logger.LogDebug(ex, "Failed shutdown cleanup for {name}", cleanupName ?? cleanupIdent);
}
}
private async Task<bool> ApplyCustomizationDataAsync(Guid applicationId, KeyValuePair<ObjectKind, HashSet<PlayerChanges>> changes, CharacterData charaData, CancellationToken token)
{
if (PlayerCharacter == nint.Zero) return false;
var ptr = PlayerCharacter;
var handler = changes.Key switch
@@ -1210,14 +1607,29 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
{
if (handler.Address == nint.Zero)
{
return;
return false;
}
Logger.LogDebug("[{applicationId}] Applying Customization Data for {handler}", applicationId, handler);
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handler, applicationId, 30000, token).ConfigureAwait(false);
await handler.IsBeingDrawnRunOnFrameworkAsync().ConfigureAwait(false);
if (handler.ObjectKind != ObjectKind.Player
&& handler.CurrentDrawCondition == GameObjectHandler.DrawCondition.DrawObjectZero)
{
Logger.LogDebug("[{applicationId}] Skipping customization apply for {handler}, draw object not available", applicationId, handler);
return false;
}
var drawTimeoutMs = handler.ObjectKind == ObjectKind.Player ? 30000 : 5000;
var fullyLoadedTimeoutMs = handler.ObjectKind == ObjectKind.Player ? FullyLoadedTimeoutMsPlayer : FullyLoadedTimeoutMsOther;
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handler, applicationId, drawTimeoutMs, token).ConfigureAwait(false);
if (handler.Address != nint.Zero)
{
await _actorObjectService.WaitForFullyLoadedAsync(handler.Address, token).ConfigureAwait(false);
var fullyLoaded = await _actorObjectService.WaitForFullyLoadedAsync(handler.Address, token, fullyLoadedTimeoutMs).ConfigureAwait(false);
if (!fullyLoaded)
{
Logger.LogDebug("[{applicationId}] Timed out waiting for {handler} to fully load, skipping customization apply", applicationId, handler);
return false;
}
}
token.ThrowIfCancellationRequested();
@@ -1281,6 +1693,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
{
await _ipcManager.Penumbra.RedrawAsync(Logger, handler, applicationId, token).ConfigureAwait(false);
}
return true;
}
finally
{
@@ -1501,6 +1915,17 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
{
await _textureDownscaleService.WaitForPendingJobsAsync(downloadedTextureHashes, downloadToken).ConfigureAwait(false);
}
var downloadedModelHashes = toDownloadReplacements
.Where(static replacement => replacement.GamePaths.Any(static path => path.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase)))
.Select(static replacement => replacement.Hash)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
if (downloadedModelHashes.Count > 0)
{
await _modelDecimationService.WaitForPendingJobsAsync(downloadedModelHashes, downloadToken).ConfigureAwait(false);
}
}
toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
@@ -1588,37 +2013,25 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
RecordFailure("Handler not available for application", "HandlerUnavailable");
return;
}
_applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource();
if (_applicationTask != null && !_applicationTask.IsCompleted)
var appToken = _applicationCancellationTokenSource?.Token;
while ((!_applicationTask?.IsCompleted ?? false)
&& !downloadToken.IsCancellationRequested
&& (!appToken?.IsCancellationRequested ?? false))
{
Logger.LogDebug("[BASE-{appBase}] Cancelling current data application (Id: {id}) for player ({handler})", applicationBase, _applicationId, PlayerName);
var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(downloadToken, timeoutCts.Token);
try
{
await _applicationTask.WaitAsync(combinedCts.Token).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
Logger.LogWarning("[BASE-{appBase}] Timeout waiting for application task {id} to complete, proceeding anyway", applicationBase, _applicationId);
}
finally
{
timeoutCts.Dispose();
combinedCts.Dispose();
}
Logger.LogDebug("[BASE-{appBase}] Waiting for current data application (Id: {id}) for player ({handler}) to finish",
applicationBase, _applicationId, PlayerName);
await Task.Delay(250).ConfigureAwait(false);
}
if (downloadToken.IsCancellationRequested)
if (downloadToken.IsCancellationRequested || (appToken?.IsCancellationRequested ?? false))
{
_forceFullReapply = true;
RecordFailure("Application cancelled", "Cancellation");
return;
}
_applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource();
var token = _applicationCancellationTokenSource.Token;
_applicationTask = ApplyCharacterDataAsync(applicationBase, handlerForApply, charaData, updatedData, updateModdedPaths, updateManip, moddedPaths, wantsModApply, pendingModReapply, token);
@@ -1641,7 +2054,17 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handlerForApply, _applicationId, 30000, token).ConfigureAwait(false);
if (handlerForApply.Address != nint.Zero)
{
await _actorObjectService.WaitForFullyLoadedAsync(handlerForApply.Address, token).ConfigureAwait(false);
var fullyLoaded = await _actorObjectService.WaitForFullyLoadedAsync(handlerForApply.Address, token, FullyLoadedTimeoutMsPlayer).ConfigureAwait(false);
if (!fullyLoaded)
{
Logger.LogDebug("[BASE-{applicationId}] Timed out waiting for {handler} to fully load, caching data for later application",
applicationBase, GetLogIdentifier());
_cachedData = charaData;
_pairStateCache.Store(Ident, charaData);
_forceFullReapply = true;
RecordFailure("Actor not fully loaded within timeout", "FullyLoadedTimeout");
return;
}
}
token.ThrowIfCancellationRequested();
@@ -1728,7 +2151,15 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
foreach (var kind in updatedData)
{
await ApplyCustomizationDataAsync(_applicationId, kind, charaData, token).ConfigureAwait(false);
var applied = await ApplyCustomizationDataAsync(_applicationId, kind, charaData, token).ConfigureAwait(false);
if (applied)
{
ClearOwnedObjectRetry(kind.Key);
}
else if (kind.Key != ObjectKind.Player)
{
ScheduleOwnedObjectRetry(kind.Key, kind.Value);
}
token.ThrowIfCancellationRequested();
}
@@ -1744,29 +2175,31 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
{
_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, new List<DownloadFileTransfer>());
}
if (LastAppliedDataTris < 0)
if (LastAppliedDataTris < 0 || LastAppliedApproximateEffectiveTris < 0)
{
await _playerPerformanceService.CheckTriangleUsageThresholds(this, charaData).ConfigureAwait(false);
}
StorePerformanceMetrics(charaData);
_lastSuccessfulDataHash = GetDataHashSafe(charaData);
_lastSuccessfulApplyAt = DateTime.UtcNow;
ClearFailureState();
Logger.LogDebug("[{applicationId}] Application finished", _applicationId);
}
catch (OperationCanceledException)
}
catch (OperationCanceledException)
{
Logger.LogDebug("[{applicationId}] Application cancelled for {handler}", _applicationId, GetLogIdentifier());
_cachedData = charaData;
_pairStateCache.Store(Ident, charaData);
_forceFullReapply = true;
RecordFailure("Application cancelled", "Cancellation");
}
catch (Exception ex)
{
if (ex is AggregateException aggr && aggr.InnerExceptions.Any(e => e is ArgumentNullException))
{
Logger.LogDebug("[{applicationId}] Application cancelled for {handler}", _applicationId, GetLogIdentifier());
_cachedData = charaData;
_pairStateCache.Store(Ident, charaData);
_forceFullReapply = true;
RecordFailure("Application cancelled", "Cancellation");
}
catch (Exception ex)
{
if (ex is AggregateException aggr && aggr.InnerExceptions.Any(e => e is ArgumentNullException))
{
IsVisible = false;
IsVisible = false;
_forceApplyMods = true;
_cachedData = charaData;
_pairStateCache.Store(Ident, charaData);
@@ -1863,6 +2296,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
{
IsVisible = false;
_charaHandler?.Invalidate();
ClearAllOwnedObjectRetries();
_downloadCancellationTokenSource?.CancelDispose();
_downloadCancellationTokenSource = null;
if (logChange)
@@ -1875,6 +2309,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
{
PlayerName = name;
_charaHandler = _gameObjectHandlerFactory.Create(ObjectKind.Player, () => _dalamudUtil.GetPlayerCharacterFromCachedTableByIdent(Ident), isWatched: false).GetAwaiter().GetResult();
UpdateLastKnownActor(_charaHandler.Address, name);
var user = GetPrimaryUserData();
if (!string.IsNullOrEmpty(user.UID))
@@ -2186,6 +2621,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
_cachedData = null;
LastAppliedDataBytes = -1;
LastAppliedDataTris = -1;
LastAppliedApproximateEffectiveTris = -1;
LastAppliedApproximateVRAMBytes = -1;
LastAppliedApproximateEffectiveVRAMBytes = -1;
}
@@ -2244,6 +2680,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
if (descriptor.Address == nint.Zero)
return;
UpdateLastKnownActor(descriptor);
RefreshTrackedHandler(descriptor);
QueueActorInitialization(descriptor);
}
@@ -2367,6 +2804,29 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
return !string.IsNullOrEmpty(hashedCid);
}
private void UpdateLastKnownActor(ActorObjectService.ActorDescriptor descriptor)
{
_lastKnownAddress = descriptor.Address;
_lastKnownObjectIndex = descriptor.ObjectIndex;
if (!string.IsNullOrEmpty(descriptor.Name))
{
_lastKnownName = descriptor.Name;
}
}
private void UpdateLastKnownActor(nint address, string? name)
{
if (address != nint.Zero)
{
_lastKnownAddress = address;
}
if (!string.IsNullOrEmpty(name))
{
_lastKnownName = name;
}
}
private static void SplitPapMappings(
Dictionary<(string GamePath, string? Hash), string> moddedPaths,
out Dictionary<(string GamePath, string? Hash), string> withoutPap,

View File

@@ -5,9 +5,11 @@ using LightlessSync.PlayerData.Factories;
using LightlessSync.Services;
using LightlessSync.Services.ActorTracking;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ModelDecimation;
using LightlessSync.Services.PairProcessing;
using LightlessSync.Services.ServerConfiguration;
using LightlessSync.Services.TextureCompression;
using Dalamud.Plugin.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
@@ -30,11 +32,13 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
private readonly PairProcessingLimiter _pairProcessingLimiter;
private readonly ServerConfigurationManager _serverConfigManager;
private readonly TextureDownscaleService _textureDownscaleService;
private readonly ModelDecimationService _modelDecimationService;
private readonly PairStateCache _pairStateCache;
private readonly PairPerformanceMetricsCache _pairPerformanceMetricsCache;
private readonly PenumbraTempCollectionJanitor _tempCollectionJanitor;
private readonly LightlessConfigService _configService;
private readonly XivDataAnalyzer _modelAnalyzer;
private readonly IFramework _framework;
public PairHandlerAdapterFactory(
ILoggerFactory loggerFactory,
@@ -45,12 +49,14 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
FileDownloadManagerFactory fileDownloadManagerFactory,
PluginWarningNotificationService pluginWarningNotificationManager,
IServiceProvider serviceProvider,
IFramework framework,
IHostApplicationLifetime lifetime,
FileCacheManager fileCacheManager,
PlayerPerformanceService playerPerformanceService,
PairProcessingLimiter pairProcessingLimiter,
ServerConfigurationManager serverConfigManager,
TextureDownscaleService textureDownscaleService,
ModelDecimationService modelDecimationService,
PairStateCache pairStateCache,
PairPerformanceMetricsCache pairPerformanceMetricsCache,
PenumbraTempCollectionJanitor tempCollectionJanitor,
@@ -65,12 +71,14 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
_fileDownloadManagerFactory = fileDownloadManagerFactory;
_pluginWarningNotificationManager = pluginWarningNotificationManager;
_serviceProvider = serviceProvider;
_framework = framework;
_lifetime = lifetime;
_fileCacheManager = fileCacheManager;
_playerPerformanceService = playerPerformanceService;
_pairProcessingLimiter = pairProcessingLimiter;
_serverConfigManager = serverConfigManager;
_textureDownscaleService = textureDownscaleService;
_modelDecimationService = modelDecimationService;
_pairStateCache = pairStateCache;
_pairPerformanceMetricsCache = pairPerformanceMetricsCache;
_tempCollectionJanitor = tempCollectionJanitor;
@@ -93,6 +101,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
downloadManager,
_pluginWarningNotificationManager,
dalamudUtilService,
_framework,
actorObjectService,
_lifetime,
_fileCacheManager,
@@ -100,6 +109,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
_pairProcessingLimiter,
_serverConfigManager,
_textureDownscaleService,
_modelDecimationService,
_pairStateCache,
_pairPerformanceMetricsCache,
_tempCollectionJanitor,

View File

@@ -89,7 +89,7 @@ public sealed class PairHandlerRegistry : IDisposable
}
if (handler.LastReceivedCharacterData is not null &&
(handler.LastAppliedApproximateVRAMBytes < 0 || handler.LastAppliedDataTris < 0))
(handler.LastAppliedApproximateVRAMBytes < 0 || handler.LastAppliedDataTris < 0 || handler.LastAppliedApproximateEffectiveTris < 0))
{
handler.ApplyLastReceivedData(forced: true);
}

View File

@@ -258,7 +258,8 @@ public sealed class PairLedger : DisposableMediatorSubscriberBase
if (handler.LastAppliedApproximateVRAMBytes >= 0
&& handler.LastAppliedDataTris >= 0
&& handler.LastAppliedApproximateEffectiveVRAMBytes >= 0)
&& handler.LastAppliedApproximateEffectiveVRAMBytes >= 0
&& handler.LastAppliedApproximateEffectiveTris >= 0)
{
continue;
}

View File

@@ -5,7 +5,8 @@ namespace LightlessSync.PlayerData.Pairs;
public readonly record struct PairPerformanceMetrics(
long TriangleCount,
long ApproximateVramBytes,
long ApproximateEffectiveVramBytes);
long ApproximateEffectiveVramBytes,
long ApproximateEffectiveTris);
/// <summary>
/// caches performance metrics keyed by pair ident

View File

@@ -50,6 +50,7 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
});
Mediator.Subscribe<ConnectedMessage>(this, (_) => PushToAllVisibleUsers());
Mediator.Subscribe<PairOnlineMessage>(this, (msg) => HandlePairOnline(msg.PairIdent));
Mediator.Subscribe<DisconnectedMessage>(this, (_) =>
{
_fileTransferManager.CancelUpload();
@@ -111,6 +112,20 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
_ = PushCharacterDataAsync(forced);
}
private void HandlePairOnline(PairUniqueIdentifier pairIdent)
{
if (!_apiController.IsConnected || !_pairLedger.IsPairVisible(pairIdent))
{
return;
}
if (_pairLedger.GetHandler(pairIdent)?.UserData is { } user)
{
_usersToPushDataTo.Add(user);
PushCharacterData(forced: true);
}
}
private async Task PushCharacterDataAsync(bool forced = false)
{
await _pushLock.WaitAsync(_runtimeCts.Token).ConfigureAwait(false);
@@ -152,5 +167,6 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
}
}
private List<UserData> GetVisibleUsers() => [.. _pairLedger.GetVisiblePairs().Select(connection => connection.User)];
private List<UserData> GetVisibleUsers()
=> [.. _pairLedger.GetVisiblePairs().Where(connection => connection.IsOnline).Select(connection => connection.User)];
}