|
|
|
|
@@ -65,6 +65,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
private readonly object _pauseLock = new();
|
|
|
|
|
private Task _pauseTransitionTask = Task.CompletedTask;
|
|
|
|
|
private bool _pauseRequested;
|
|
|
|
|
private DateTime? _lastDataReceivedAt;
|
|
|
|
|
private DateTime? _lastApplyAttemptAt;
|
|
|
|
|
private DateTime? _lastSuccessfulApplyAt;
|
|
|
|
|
private string? _lastFailureReason;
|
|
|
|
|
private IReadOnlyList<string> _lastBlockingConditions = Array.Empty<string>();
|
|
|
|
|
|
|
|
|
|
public string Ident { get; }
|
|
|
|
|
public bool Initialized { get; private set; }
|
|
|
|
|
@@ -101,6 +106,15 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
public long LastAppliedApproximateVRAMBytes { get; set; } = -1;
|
|
|
|
|
public long LastAppliedApproximateEffectiveVRAMBytes { get; set; } = -1;
|
|
|
|
|
public CharacterData? LastReceivedCharacterData { get; private set; }
|
|
|
|
|
public DateTime? LastDataReceivedAt => _lastDataReceivedAt;
|
|
|
|
|
public DateTime? LastApplyAttemptAt => _lastApplyAttemptAt;
|
|
|
|
|
public DateTime? LastSuccessfulApplyAt => _lastSuccessfulApplyAt;
|
|
|
|
|
public string? LastFailureReason => _lastFailureReason;
|
|
|
|
|
public IReadOnlyList<string> LastBlockingConditions => _lastBlockingConditions;
|
|
|
|
|
public bool IsApplying => _applicationTask is { IsCompleted: false };
|
|
|
|
|
public bool IsDownloading => _downloadManager.IsDownloading;
|
|
|
|
|
public int PendingDownloadCount => _downloadManager.CurrentDownloads.Count;
|
|
|
|
|
public int ForbiddenDownloadCount => _downloadManager.ForbiddenTransfers.Count;
|
|
|
|
|
|
|
|
|
|
public PairHandlerAdapter(
|
|
|
|
|
ILogger<PairHandlerAdapter> logger,
|
|
|
|
|
@@ -423,6 +437,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
{
|
|
|
|
|
EnsureInitialized();
|
|
|
|
|
LastReceivedCharacterData = data;
|
|
|
|
|
_lastDataReceivedAt = DateTime.UtcNow;
|
|
|
|
|
ApplyLastReceivedData();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -713,10 +728,26 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
&& _ipcManager.Glamourer.APIAvailable;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void RecordFailure(string reason, params string[] conditions)
|
|
|
|
|
{
|
|
|
|
|
_lastFailureReason = reason;
|
|
|
|
|
_lastBlockingConditions = conditions.Length == 0 ? Array.Empty<string>() : conditions.ToArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ClearFailureState()
|
|
|
|
|
{
|
|
|
|
|
_lastFailureReason = null;
|
|
|
|
|
_lastBlockingConditions = Array.Empty<string>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void ApplyCharacterData(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization = false)
|
|
|
|
|
{
|
|
|
|
|
_lastApplyAttemptAt = DateTime.UtcNow;
|
|
|
|
|
ClearFailureState();
|
|
|
|
|
|
|
|
|
|
if (characterData is null)
|
|
|
|
|
{
|
|
|
|
|
RecordFailure("Received null character data", "InvalidData");
|
|
|
|
|
Logger.LogWarning("[BASE-{appBase}] Received null character data, skipping application for {handler}", applicationBase, GetLogIdentifier());
|
|
|
|
|
SetUploading(false);
|
|
|
|
|
return;
|
|
|
|
|
@@ -725,9 +756,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
var user = GetPrimaryUserData();
|
|
|
|
|
if (_dalamudUtil.IsInCombat)
|
|
|
|
|
{
|
|
|
|
|
const string reason = "Cannot apply character data: you are in combat, deferring application";
|
|
|
|
|
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
|
|
|
|
"Cannot apply character data: you are in combat, deferring application")));
|
|
|
|
|
reason)));
|
|
|
|
|
Logger.LogDebug("[BASE-{appBase}] Received data but player is in combat", applicationBase);
|
|
|
|
|
RecordFailure(reason, "Combat");
|
|
|
|
|
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
|
|
|
|
SetUploading(false);
|
|
|
|
|
return;
|
|
|
|
|
@@ -735,9 +768,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
|
|
|
|
|
if (_dalamudUtil.IsPerforming)
|
|
|
|
|
{
|
|
|
|
|
const string reason = "Cannot apply character data: you are performing music, deferring application";
|
|
|
|
|
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
|
|
|
|
"Cannot apply character data: you are performing music, deferring application")));
|
|
|
|
|
reason)));
|
|
|
|
|
Logger.LogDebug("[BASE-{appBase}] Received data but player is performing", applicationBase);
|
|
|
|
|
RecordFailure(reason, "Performance");
|
|
|
|
|
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
|
|
|
|
SetUploading(false);
|
|
|
|
|
return;
|
|
|
|
|
@@ -745,9 +780,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
|
|
|
|
|
if (_dalamudUtil.IsInInstance)
|
|
|
|
|
{
|
|
|
|
|
const string reason = "Cannot apply character data: you are in an instance, deferring application";
|
|
|
|
|
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
|
|
|
|
"Cannot apply character data: you are in an instance, deferring application")));
|
|
|
|
|
reason)));
|
|
|
|
|
Logger.LogDebug("[BASE-{appBase}] Received data but player is in instance", applicationBase);
|
|
|
|
|
RecordFailure(reason, "Instance");
|
|
|
|
|
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
|
|
|
|
SetUploading(false);
|
|
|
|
|
return;
|
|
|
|
|
@@ -755,9 +792,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
|
|
|
|
|
if (_dalamudUtil.IsInCutscene)
|
|
|
|
|
{
|
|
|
|
|
const string reason = "Cannot apply character data: you are in a cutscene, deferring application";
|
|
|
|
|
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
|
|
|
|
"Cannot apply character data: you are in a cutscene, deferring application")));
|
|
|
|
|
reason)));
|
|
|
|
|
Logger.LogDebug("[BASE-{appBase}] Received data but player is in a cutscene", applicationBase);
|
|
|
|
|
RecordFailure(reason, "Cutscene");
|
|
|
|
|
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
|
|
|
|
SetUploading(false);
|
|
|
|
|
return;
|
|
|
|
|
@@ -765,9 +804,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
|
|
|
|
|
if (_dalamudUtil.IsInGpose)
|
|
|
|
|
{
|
|
|
|
|
const string reason = "Cannot apply character data: you are in GPose, deferring application";
|
|
|
|
|
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
|
|
|
|
"Cannot apply character data: you are in GPose, deferring application")));
|
|
|
|
|
reason)));
|
|
|
|
|
Logger.LogDebug("[BASE-{appBase}] Received data but player is in GPose", applicationBase);
|
|
|
|
|
RecordFailure(reason, "GPose");
|
|
|
|
|
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
|
|
|
|
SetUploading(false);
|
|
|
|
|
return;
|
|
|
|
|
@@ -775,9 +816,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
|
|
|
|
|
if (!_ipcManager.Penumbra.APIAvailable || !_ipcManager.Glamourer.APIAvailable)
|
|
|
|
|
{
|
|
|
|
|
const string reason = "Cannot apply character data: Penumbra or Glamourer is not available, deferring application";
|
|
|
|
|
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
|
|
|
|
|
"Cannot apply character data: Penumbra or Glamourer is not available, deferring application")));
|
|
|
|
|
reason)));
|
|
|
|
|
Logger.LogInformation("[BASE-{appbase}] Application of data for {player} while Penumbra/Glamourer unavailable, returning", applicationBase, GetLogIdentifier());
|
|
|
|
|
RecordFailure(reason, "PluginUnavailable");
|
|
|
|
|
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
|
|
|
|
|
SetUploading(false);
|
|
|
|
|
return;
|
|
|
|
|
@@ -1260,6 +1303,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
|
|
|
|
|
if (!_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, toDownloadFiles))
|
|
|
|
|
{
|
|
|
|
|
RecordFailure("Auto pause triggered by VRAM usage thresholds", "VRAMThreshold");
|
|
|
|
|
_downloadManager.ClearDownload();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
@@ -1272,9 +1316,24 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
if (downloadToken.IsCancellationRequested)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogTrace("[BASE-{appBase}] Detected cancellation", applicationBase);
|
|
|
|
|
RecordFailure("Download cancelled", "Cancellation");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!skipDownscaleForPair)
|
|
|
|
|
{
|
|
|
|
|
var downloadedTextureHashes = toDownloadReplacements
|
|
|
|
|
.Where(static replacement => replacement.GamePaths.Any(static path => path.EndsWith(".tex", StringComparison.OrdinalIgnoreCase)))
|
|
|
|
|
.Select(static replacement => replacement.Hash)
|
|
|
|
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
if (downloadedTextureHashes.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
await _textureDownscaleService.WaitForPendingJobsAsync(downloadedTextureHashes, downloadToken).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
|
|
|
|
|
|
|
|
|
|
if (toDownloadReplacements.TrueForAll(c => _downloadManager.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, c.Hash, StringComparison.Ordinal))))
|
|
|
|
|
@@ -1287,6 +1346,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
|
|
|
|
|
if (!await _playerPerformanceService.CheckBothThresholds(this, charaData).ConfigureAwait(false))
|
|
|
|
|
{
|
|
|
|
|
RecordFailure("Auto pause triggered by performance thresholds", "PerformanceThreshold");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@@ -1307,6 +1367,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
_cachedData = charaData;
|
|
|
|
|
_pairStateCache.Store(Ident, charaData);
|
|
|
|
|
_forceFullReapply = true;
|
|
|
|
|
RecordFailure("Handler not available for application", "HandlerUnavailable");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -1322,6 +1383,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
if (downloadToken.IsCancellationRequested || (appToken?.IsCancellationRequested ?? false))
|
|
|
|
|
{
|
|
|
|
|
_forceFullReapply = true;
|
|
|
|
|
RecordFailure("Application cancelled", "Cancellation");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -1359,6 +1421,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
_cachedData = charaData;
|
|
|
|
|
_pairStateCache.Store(Ident, charaData);
|
|
|
|
|
_forceFullReapply = true;
|
|
|
|
|
RecordFailure("Penumbra collection unavailable", "PenumbraUnavailable");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@@ -1378,6 +1441,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
_cachedData = charaData;
|
|
|
|
|
_pairStateCache.Store(Ident, charaData);
|
|
|
|
|
_forceFullReapply = true;
|
|
|
|
|
RecordFailure("Game object not available for application", "GameObjectUnavailable");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -1414,41 +1478,45 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
_needsCollectionRebuild = false;
|
|
|
|
|
if (LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0)
|
|
|
|
|
{
|
|
|
|
|
_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, new List<DownloadFileTransfer>());
|
|
|
|
|
}
|
|
|
|
|
if (LastAppliedDataTris < 0)
|
|
|
|
|
{
|
|
|
|
|
await _playerPerformanceService.CheckTriangleUsageThresholds(this, charaData).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
StorePerformanceMetrics(charaData);
|
|
|
|
|
Logger.LogDebug("[{applicationId}] Application finished", _applicationId);
|
|
|
|
|
_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, new List<DownloadFileTransfer>());
|
|
|
|
|
}
|
|
|
|
|
catch (OperationCanceledException)
|
|
|
|
|
if (LastAppliedDataTris < 0)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogDebug("[{applicationId}] Application cancelled for {handler}", _applicationId, GetLogIdentifier());
|
|
|
|
|
await _playerPerformanceService.CheckTriangleUsageThresholds(this, charaData).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
StorePerformanceMetrics(charaData);
|
|
|
|
|
_lastSuccessfulApplyAt = DateTime.UtcNow;
|
|
|
|
|
ClearFailureState();
|
|
|
|
|
Logger.LogDebug("[{applicationId}] Application finished", _applicationId);
|
|
|
|
|
}
|
|
|
|
|
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))
|
|
|
|
|
{
|
|
|
|
|
IsVisible = false;
|
|
|
|
|
_forceApplyMods = true;
|
|
|
|
|
_cachedData = charaData;
|
|
|
|
|
_pairStateCache.Store(Ident, charaData);
|
|
|
|
|
_forceFullReapply = true;
|
|
|
|
|
Logger.LogDebug("[{applicationId}] Cancelled, player turned null during application", _applicationId);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
if (ex is AggregateException aggr && aggr.InnerExceptions.Any(e => e is ArgumentNullException))
|
|
|
|
|
{
|
|
|
|
|
IsVisible = false;
|
|
|
|
|
_forceApplyMods = true;
|
|
|
|
|
_cachedData = charaData;
|
|
|
|
|
_pairStateCache.Store(Ident, charaData);
|
|
|
|
|
_forceFullReapply = true;
|
|
|
|
|
Logger.LogDebug("[{applicationId}] Cancelled, player turned null during application", _applicationId);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Logger.LogWarning(ex, "[{applicationId}] Cancelled", _applicationId);
|
|
|
|
|
_forceFullReapply = true;
|
|
|
|
|
}
|
|
|
|
|
Logger.LogWarning(ex, "[{applicationId}] Cancelled", _applicationId);
|
|
|
|
|
_forceFullReapply = true;
|
|
|
|
|
}
|
|
|
|
|
RecordFailure($"Application failed: {ex.Message}", "Exception");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void FrameworkUpdate()
|
|
|
|
|
{
|
|
|
|
|
|