From 28967d6e17f3f17bddfa480d7b4456394cfba7d7 Mon Sep 17 00:00:00 2001 From: azyges Date: Sat, 29 Nov 2025 09:17:28 +0900 Subject: [PATCH] rebuild the temp collection if cached files don't persist --- .../PlayerData/Pairs/PairHandlerAdapter.cs | 148 +++++++++++++----- 1 file changed, 106 insertions(+), 42 deletions(-) diff --git a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs index d08cc19..ad77bcb 100644 --- a/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs +++ b/LightlessSync/PlayerData/Pairs/PairHandlerAdapter.cs @@ -77,6 +77,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa private CancellationTokenSource? _downloadCancellationTokenSource = new(); private bool _forceApplyMods = false; private bool _forceFullReapply; + private Dictionary<(string GamePath, string? Hash), string>? _lastAppliedModdedPaths; + private bool _needsCollectionRebuild; private bool _isVisible; private Guid _penumbraCollection; private readonly object _collectionGate = new(); @@ -349,12 +351,14 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa private void ResetPenumbraCollection(bool releaseFromPenumbra = true, string? reason = null) { Guid toRelease = Guid.Empty; + bool hadCollection = false; lock (_collectionGate) { if (_penumbraCollection != Guid.Empty) { toRelease = _penumbraCollection; _penumbraCollection = Guid.Empty; + hadCollection = true; } } @@ -362,6 +366,13 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa if (cached.HasValue && cached.Value != Guid.Empty) { toRelease = cached.Value; + hadCollection = true; + } + + if (hadCollection) + { + _needsCollectionRebuild = true; + _forceFullReapply = true; } if (!releaseFromPenumbra || toRelease == Guid.Empty || !_ipcManager.Penumbra.APIAvailable) @@ -600,6 +611,25 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa return data; } + private bool HasValidCachedModdedPaths() + { + if (_lastAppliedModdedPaths is null || _lastAppliedModdedPaths.Count == 0) + { + return false; + } + + foreach (var entry in _lastAppliedModdedPaths) + { + if (string.IsNullOrEmpty(entry.Value) || !File.Exists(entry.Value)) + { + Logger.LogDebug("Cached file path {path} missing for {handler}, forcing recalculation", entry.Value ?? "empty", GetLogIdentifier()); + return false; + } + } + + return true; + } + private bool CanApplyNow() { return !_dalamudUtil.IsInCombat @@ -844,6 +874,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa { PlayerName = null; _cachedData = null; + _lastAppliedModdedPaths = null; + _needsCollectionRebuild = false; Logger.LogDebug("Disposing {name} complete", name); } } @@ -1012,73 +1044,103 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa var updateModdedPaths = updatedData.Values.Any(v => v.Any(p => p == PlayerChanges.ModFiles)); var updateManip = updatedData.Values.Any(v => v.Any(p => p == PlayerChanges.ModManip)); + var needsCollectionRebuild = _needsCollectionRebuild; + var reuseCachedModdedPaths = !updateModdedPaths && needsCollectionRebuild && _lastAppliedModdedPaths is not null; + updateModdedPaths = updateModdedPaths || needsCollectionRebuild; + updateManip = updateManip || needsCollectionRebuild; + Dictionary<(string GamePath, string? Hash), string>? cachedModdedPaths = null; + if (reuseCachedModdedPaths) + { + if (HasValidCachedModdedPaths()) + { + cachedModdedPaths = _lastAppliedModdedPaths; + } + else + { + Logger.LogDebug("{handler}: Cached files missing, recalculating mappings", GetLogIdentifier()); + _lastAppliedModdedPaths = null; + } + } _downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate() ?? new CancellationTokenSource(); var downloadToken = _downloadCancellationTokenSource.Token; - _ = DownloadAndApplyCharacterAsync(applicationBase, charaData, updatedData, updateModdedPaths, updateManip, downloadToken).ConfigureAwait(false); + _ = DownloadAndApplyCharacterAsync(applicationBase, charaData, updatedData, updateModdedPaths, updateManip, cachedModdedPaths, downloadToken).ConfigureAwait(false); } private Task? _pairDownloadTask; private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary> updatedData, - bool updateModdedPaths, bool updateManip, CancellationToken downloadToken) + bool updateModdedPaths, bool updateManip, Dictionary<(string GamePath, string? Hash), string>? cachedModdedPaths, CancellationToken downloadToken) { await using var concurrencyLease = await _pairProcessingLimiter.AcquireAsync(downloadToken).ConfigureAwait(false); - Dictionary<(string GamePath, string? Hash), string> moddedPaths = []; bool skipDownscaleForPair = ShouldSkipDownscale(); var user = GetPrimaryUserData(); + Dictionary<(string GamePath, string? Hash), string> moddedPaths; if (updateModdedPaths) { - int attempts = 0; - List toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken); - - while (toDownloadReplacements.Count > 0 && attempts++ <= 10 && !downloadToken.IsCancellationRequested) + if (cachedModdedPaths is not null) { - if (_pairDownloadTask != null && !_pairDownloadTask.IsCompleted) + moddedPaths = new Dictionary<(string GamePath, string? Hash), string>(cachedModdedPaths, cachedModdedPaths.Comparer); + } + else + { + int attempts = 0; + List toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken); + + while (toDownloadReplacements.Count > 0 && attempts++ <= 10 && !downloadToken.IsCancellationRequested) { - Logger.LogDebug("[BASE-{appBase}] Finishing prior running download task for player {name}, {kind}", applicationBase, PlayerName, updatedData); + if (_pairDownloadTask != null && !_pairDownloadTask.IsCompleted) + { + Logger.LogDebug("[BASE-{appBase}] Finishing prior running download task for player {name}, {kind}", applicationBase, PlayerName, updatedData); + await _pairDownloadTask.ConfigureAwait(false); + } + + Logger.LogDebug("[BASE-{appBase}] Downloading missing files for player {name}, {kind}", applicationBase, PlayerName, updatedData); + + Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Informational, + $"Starting download for {toDownloadReplacements.Count} files"))); + var toDownloadFiles = await _downloadManager.InitiateDownloadList(_charaHandler!, toDownloadReplacements, downloadToken).ConfigureAwait(false); + + if (!_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, toDownloadFiles)) + { + _downloadManager.ClearDownload(); + return; + } + + var handlerForDownload = _charaHandler; + _pairDownloadTask = Task.Run(async () => await _downloadManager.DownloadFiles(handlerForDownload, toDownloadReplacements, downloadToken, skipDownscaleForPair).ConfigureAwait(false)); + await _pairDownloadTask.ConfigureAwait(false); + + if (downloadToken.IsCancellationRequested) + { + Logger.LogTrace("[BASE-{appBase}] Detected cancellation", applicationBase); + return; + } + + toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken); + + if (toDownloadReplacements.TrueForAll(c => _downloadManager.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, c.Hash, StringComparison.Ordinal)))) + { + break; + } + + await Task.Delay(TimeSpan.FromSeconds(2), downloadToken).ConfigureAwait(false); } - Logger.LogDebug("[BASE-{appBase}] Downloading missing files for player {name}, {kind}", applicationBase, PlayerName, updatedData); - - Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Informational, - $"Starting download for {toDownloadReplacements.Count} files"))); - var toDownloadFiles = await _downloadManager.InitiateDownloadList(_charaHandler!, toDownloadReplacements, downloadToken).ConfigureAwait(false); - - if (!_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, toDownloadFiles)) + if (!await _playerPerformanceService.CheckBothThresholds(this, charaData).ConfigureAwait(false)) { - _downloadManager.ClearDownload(); return; } - - var handlerForDownload = _charaHandler; - _pairDownloadTask = Task.Run(async () => await _downloadManager.DownloadFiles(handlerForDownload, toDownloadReplacements, downloadToken, skipDownscaleForPair).ConfigureAwait(false)); - - await _pairDownloadTask.ConfigureAwait(false); - - if (downloadToken.IsCancellationRequested) - { - Logger.LogTrace("[BASE-{appBase}] Detected cancellation", applicationBase); - return; - } - - toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken); - - if (toDownloadReplacements.TrueForAll(c => _downloadManager.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, c.Hash, StringComparison.Ordinal)))) - { - break; - } - - await Task.Delay(TimeSpan.FromSeconds(2), downloadToken).ConfigureAwait(false); - } - - if (!await _playerPerformanceService.CheckBothThresholds(this, charaData).ConfigureAwait(false)) - { - return; } } + else + { + moddedPaths = cachedModdedPaths is not null + ? new Dictionary<(string GamePath, string? Hash), string>(cachedModdedPaths, cachedModdedPaths.Comparer) + : []; + } downloadToken.ThrowIfCancellationRequested(); @@ -1162,6 +1224,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa await _ipcManager.Penumbra.SetTemporaryModsAsync(Logger, _applicationId, penumbraCollection, moddedPaths.ToDictionary(k => k.Key.GamePath, k => k.Value, StringComparer.Ordinal)).ConfigureAwait(false); + _lastAppliedModdedPaths = new Dictionary<(string GamePath, string? Hash), string>(moddedPaths, moddedPaths.Comparer); LastAppliedDataBytes = -1; foreach (var path in moddedPaths.Values.Distinct(StringComparer.OrdinalIgnoreCase).Select(v => new FileInfo(v)).Where(p => p.Exists)) { @@ -1187,6 +1250,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa _cachedData = charaData; _pairStateCache.Store(Ident, charaData); _forceFullReapply = false; + _needsCollectionRebuild = false; if (LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0) { _playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, new List());