This commit is contained in:
cake
2025-11-29 04:52:56 +01:00

View File

@@ -77,6 +77,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private CancellationTokenSource? _downloadCancellationTokenSource = new(); private CancellationTokenSource? _downloadCancellationTokenSource = new();
private bool _forceApplyMods = false; private bool _forceApplyMods = false;
private bool _forceFullReapply; private bool _forceFullReapply;
private Dictionary<(string GamePath, string? Hash), string>? _lastAppliedModdedPaths;
private bool _needsCollectionRebuild;
private bool _isVisible; private bool _isVisible;
private Guid _penumbraCollection; private Guid _penumbraCollection;
private readonly object _collectionGate = new(); private readonly object _collectionGate = new();
@@ -349,12 +351,14 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private void ResetPenumbraCollection(bool releaseFromPenumbra = true, string? reason = null) private void ResetPenumbraCollection(bool releaseFromPenumbra = true, string? reason = null)
{ {
Guid toRelease = Guid.Empty; Guid toRelease = Guid.Empty;
bool hadCollection = false;
lock (_collectionGate) lock (_collectionGate)
{ {
if (_penumbraCollection != Guid.Empty) if (_penumbraCollection != Guid.Empty)
{ {
toRelease = _penumbraCollection; toRelease = _penumbraCollection;
_penumbraCollection = Guid.Empty; _penumbraCollection = Guid.Empty;
hadCollection = true;
} }
} }
@@ -362,6 +366,13 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
if (cached.HasValue && cached.Value != Guid.Empty) if (cached.HasValue && cached.Value != Guid.Empty)
{ {
toRelease = cached.Value; toRelease = cached.Value;
hadCollection = true;
}
if (hadCollection)
{
_needsCollectionRebuild = true;
_forceFullReapply = true;
} }
if (!releaseFromPenumbra || toRelease == Guid.Empty || !_ipcManager.Penumbra.APIAvailable) if (!releaseFromPenumbra || toRelease == Guid.Empty || !_ipcManager.Penumbra.APIAvailable)
@@ -600,6 +611,25 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
return data; 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() private bool CanApplyNow()
{ {
return !_dalamudUtil.IsInCombat return !_dalamudUtil.IsInCombat
@@ -844,6 +874,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
{ {
PlayerName = null; PlayerName = null;
_cachedData = null; _cachedData = null;
_lastAppliedModdedPaths = null;
_needsCollectionRebuild = false;
Logger.LogDebug("Disposing {name} complete", name); 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 updateModdedPaths = updatedData.Values.Any(v => v.Any(p => p == PlayerChanges.ModFiles));
var updateManip = updatedData.Values.Any(v => v.Any(p => p == PlayerChanges.ModManip)); 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(); _downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate() ?? new CancellationTokenSource();
var downloadToken = _downloadCancellationTokenSource.Token; 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 Task? _pairDownloadTask;
private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData, private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> 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); await using var concurrencyLease = await _pairProcessingLimiter.AcquireAsync(downloadToken).ConfigureAwait(false);
Dictionary<(string GamePath, string? Hash), string> moddedPaths = [];
bool skipDownscaleForPair = ShouldSkipDownscale(); bool skipDownscaleForPair = ShouldSkipDownscale();
var user = GetPrimaryUserData(); var user = GetPrimaryUserData();
Dictionary<(string GamePath, string? Hash), string> moddedPaths;
if (updateModdedPaths) if (updateModdedPaths)
{ {
int attempts = 0; if (cachedModdedPaths is not null)
List<FileReplacementData> toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
while (toDownloadReplacements.Count > 0 && attempts++ <= 10 && !downloadToken.IsCancellationRequested)
{ {
if (_pairDownloadTask != null && !_pairDownloadTask.IsCompleted) moddedPaths = new Dictionary<(string GamePath, string? Hash), string>(cachedModdedPaths, cachedModdedPaths.Comparer);
}
else
{
int attempts = 0;
List<FileReplacementData> 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); 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); if (!await _playerPerformanceService.CheckBothThresholds(this, charaData).ConfigureAwait(false))
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; 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(); downloadToken.ThrowIfCancellationRequested();
@@ -1162,6 +1224,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
await _ipcManager.Penumbra.SetTemporaryModsAsync(Logger, _applicationId, penumbraCollection, await _ipcManager.Penumbra.SetTemporaryModsAsync(Logger, _applicationId, penumbraCollection,
moddedPaths.ToDictionary(k => k.Key.GamePath, k => k.Value, StringComparer.Ordinal)).ConfigureAwait(false); 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; LastAppliedDataBytes = -1;
foreach (var path in moddedPaths.Values.Distinct(StringComparer.OrdinalIgnoreCase).Select(v => new FileInfo(v)).Where(p => p.Exists)) 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; _cachedData = charaData;
_pairStateCache.Store(Ident, charaData); _pairStateCache.Store(Ident, charaData);
_forceFullReapply = false; _forceFullReapply = false;
_needsCollectionRebuild = false;
if (LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0) if (LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0)
{ {
_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, new List<DownloadFileTransfer>()); _playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, new List<DownloadFileTransfer>());