This commit is contained in:
2025-12-16 06:31:29 +09:00
parent bdfcf254a8
commit 4444a88746
32 changed files with 1204 additions and 464 deletions

View File

@@ -16,6 +16,15 @@ public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
new string? PlayerName { get; }
string PlayerNameHash { get; }
uint PlayerCharacterId { get; }
DateTime? LastDataReceivedAt { get; }
DateTime? LastApplyAttemptAt { get; }
DateTime? LastSuccessfulApplyAt { get; }
string? LastFailureReason { get; }
IReadOnlyList<string> LastBlockingConditions { get; }
bool IsApplying { get; }
bool IsDownloading { get; }
int PendingDownloadCount { get; }
int ForbiddenDownloadCount { get; }
void Initialize();
void ApplyData(CharacterData data);

View File

@@ -189,4 +189,28 @@ public class Pair
handler.SetUploading(true);
}
public PairDebugInfo GetDebugInfo()
{
var handler = TryGetHandler();
if (handler is null)
{
return PairDebugInfo.Empty;
}
return new PairDebugInfo(
true,
handler.Initialized,
handler.IsVisible,
handler.ScheduledForDeletion,
handler.LastDataReceivedAt,
handler.LastApplyAttemptAt,
handler.LastSuccessfulApplyAt,
handler.LastFailureReason,
handler.LastBlockingConditions,
handler.IsApplying,
handler.IsDownloading,
handler.PendingDownloadCount,
handler.ForbiddenDownloadCount);
}
}

View File

@@ -0,0 +1,32 @@
namespace LightlessSync.PlayerData.Pairs;
public sealed record PairDebugInfo(
bool HasHandler,
bool HandlerInitialized,
bool HandlerVisible,
bool HandlerScheduledForDeletion,
DateTime? LastDataReceivedAt,
DateTime? LastApplyAttemptAt,
DateTime? LastSuccessfulApplyAt,
string? LastFailureReason,
IReadOnlyList<string> BlockingConditions,
bool IsApplying,
bool IsDownloading,
int PendingDownloadCount,
int ForbiddenDownloadCount)
{
public static PairDebugInfo Empty { get; } = new(
false,
false,
false,
false,
null,
null,
null,
null,
Array.Empty<string>(),
false,
false,
0,
0);
}

View File

@@ -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()
{

View File

@@ -69,6 +69,10 @@ public sealed class PairHandlerRegistry : IDisposable
handler = entry.Handler;
handler.ScheduledForDeletion = false;
entry.AddPair(registration.PairIdent);
if (!handler.Initialized)
{
handler.Initialize();
}
}
ApplyPauseStateForHandler(handler);
@@ -169,6 +173,7 @@ public sealed class PairHandlerRegistry : IDisposable
return PairOperationResult.Ok();
}
handler.ApplyData(dto.CharaData);
return PairOperationResult.Ok();
}