|
|
|
|
@@ -60,6 +60,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
private Guid _applicationId;
|
|
|
|
|
private Task? _applicationTask;
|
|
|
|
|
private CharacterData? _cachedData = null;
|
|
|
|
|
private CharacterData? _lastAppliedData = null;
|
|
|
|
|
private GameObjectHandler? _charaHandler;
|
|
|
|
|
private readonly Dictionary<ObjectKind, Guid?> _customizeIds = [];
|
|
|
|
|
private CombatData? _dataReceivedInDowntime;
|
|
|
|
|
@@ -256,6 +257,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
}
|
|
|
|
|
Mediator.Subscribe<ZoneSwitchStartMessage>(this, _ =>
|
|
|
|
|
{
|
|
|
|
|
LogDownloadCancellation("zone switch start");
|
|
|
|
|
_downloadCancellationTokenSource?.CancelDispose();
|
|
|
|
|
_charaHandler?.Invalidate();
|
|
|
|
|
IsVisible = false;
|
|
|
|
|
@@ -385,6 +387,42 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
return string.Equals(alias, Ident, StringComparison.Ordinal) ? alias : $"{alias} ({Ident})";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void LogDownloadCancellation(string reason, Guid? applicationBase = null)
|
|
|
|
|
{
|
|
|
|
|
if (_downloadCancellationTokenSource is null)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var inFlight = _pairDownloadTask is { IsCompleted: false };
|
|
|
|
|
if (inFlight)
|
|
|
|
|
{
|
|
|
|
|
if (applicationBase.HasValue)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogDebug("[BASE-{appBase}] {handler}: Cancelling in-flight download ({reason})",
|
|
|
|
|
applicationBase.Value, GetLogIdentifier(), reason);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Logger.LogDebug("{handler}: Cancelling in-flight download ({reason})",
|
|
|
|
|
GetLogIdentifier(), reason);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
if (applicationBase.HasValue)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogDebug("[BASE-{appBase}] {handler}: Cancelling download token ({reason}, in-flight={inFlight})",
|
|
|
|
|
applicationBase.Value, GetLogIdentifier(), reason, inFlight);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Logger.LogDebug("{handler}: Cancelling download token ({reason}, in-flight={inFlight})",
|
|
|
|
|
GetLogIdentifier(), reason, inFlight);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Task<Guid> EnsurePenumbraCollectionAsync()
|
|
|
|
|
{
|
|
|
|
|
if (_penumbraCollection != Guid.Empty)
|
|
|
|
|
@@ -851,6 +889,40 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
private bool IsForbiddenHash(string hash)
|
|
|
|
|
=> _downloadManager.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, hash, StringComparison.Ordinal));
|
|
|
|
|
|
|
|
|
|
private bool HasMissingFiles(CharacterData data)
|
|
|
|
|
{
|
|
|
|
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
|
foreach (var replacement in data.FileReplacements.SelectMany(k => k.Value))
|
|
|
|
|
{
|
|
|
|
|
if (!string.IsNullOrEmpty(replacement.FileSwapPath))
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var hash = replacement.Hash;
|
|
|
|
|
if (string.IsNullOrWhiteSpace(hash) || !seen.Add(hash))
|
|
|
|
|
{
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var fileCache = _fileDbManager.GetFileCacheByHash(hash);
|
|
|
|
|
if (fileCache is null || !File.Exists(fileCache.ResolvedFilepath))
|
|
|
|
|
{
|
|
|
|
|
if (fileCache is not null)
|
|
|
|
|
{
|
|
|
|
|
_fileDbManager.RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!IsForbiddenHash(hash))
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static bool IsNonPriorityModPath(string? gamePath)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrEmpty(gamePath))
|
|
|
|
|
@@ -1012,10 +1084,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
"Cannot apply character data: Receiving Player is in an invalid state, deferring application")));
|
|
|
|
|
Logger.LogDebug("[BASE-{appBase}] Received data but player was in invalid state, charaHandlerIsNull: {charaIsNull}, playerPointerIsNull: {ptrIsNull}",
|
|
|
|
|
applicationBase, _charaHandler == null, PlayerCharacter == IntPtr.Zero);
|
|
|
|
|
var hasDiffMods = characterData.CheckUpdatedData(applicationBase, _cachedData, Logger,
|
|
|
|
|
var hasDiffMods = characterData.CheckUpdatedData(applicationBase, _lastAppliedData, Logger,
|
|
|
|
|
this, forceApplyCustomization, forceApplyMods: false)
|
|
|
|
|
.Any(p => p.Value.Contains(PlayerChanges.ModManip) || p.Value.Contains(PlayerChanges.ModFiles));
|
|
|
|
|
_forceApplyMods = hasDiffMods || _forceApplyMods || _cachedData == null;
|
|
|
|
|
_forceApplyMods = hasDiffMods || _forceApplyMods || _lastAppliedData == null;
|
|
|
|
|
_pendingModReapply = true;
|
|
|
|
|
_cachedData = characterData;
|
|
|
|
|
Logger.LogDebug("[BASE-{appBase}] Setting data: {hash}, forceApplyMods: {force}", applicationBase, _cachedData.DataHash.Value, _forceApplyMods);
|
|
|
|
|
return;
|
|
|
|
|
@@ -1023,27 +1096,34 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
|
|
|
|
|
SetUploading(false);
|
|
|
|
|
|
|
|
|
|
Logger.LogDebug("[BASE-{appbase}] Applying data for {player}, forceApplyCustomization: {forced}, forceApplyMods: {forceMods}", applicationBase, GetLogIdentifier(), forceApplyCustomization, _forceApplyMods);
|
|
|
|
|
Logger.LogDebug("[BASE-{appbase}] Hash for data is {newHash}, last applied hash is {oldHash}", applicationBase, characterData.DataHash.Value, _lastAppliedData?.DataHash.Value ?? "NODATA");
|
|
|
|
|
|
|
|
|
|
var hasMissingFiles = false;
|
|
|
|
|
if (string.Equals(characterData.DataHash.Value, _lastAppliedData?.DataHash.Value ?? string.Empty, StringComparison.Ordinal)
|
|
|
|
|
&& !forceApplyCustomization
|
|
|
|
|
&& !_forceApplyMods
|
|
|
|
|
&& !_pendingModReapply)
|
|
|
|
|
{
|
|
|
|
|
hasMissingFiles = HasMissingFiles(characterData);
|
|
|
|
|
if (!hasMissingFiles)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_pendingModReapply = false;
|
|
|
|
|
_lastModApplyDeferred = false;
|
|
|
|
|
_lastMissingCriticalMods = 0;
|
|
|
|
|
_lastMissingNonCriticalMods = 0;
|
|
|
|
|
_lastMissingForbiddenMods = 0;
|
|
|
|
|
|
|
|
|
|
Logger.LogDebug("[BASE-{appbase}] Applying data for {player}, forceApplyCustomization: {forced}, forceApplyMods: {forceMods}", applicationBase, GetLogIdentifier(), forceApplyCustomization, _forceApplyMods);
|
|
|
|
|
Logger.LogDebug("[BASE-{appbase}] Hash for data is {newHash}, current cache hash is {oldHash}", applicationBase, characterData.DataHash.Value, _cachedData?.DataHash.Value ?? "NODATA");
|
|
|
|
|
|
|
|
|
|
if (string.Equals(characterData.DataHash.Value, _cachedData?.DataHash.Value ?? string.Empty, StringComparison.Ordinal)
|
|
|
|
|
&& !forceApplyCustomization)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Informational,
|
|
|
|
|
"Applying Character Data")));
|
|
|
|
|
|
|
|
|
|
_forceApplyMods |= forceApplyCustomization;
|
|
|
|
|
_forceApplyMods |= forceApplyCustomization || hasMissingFiles;
|
|
|
|
|
|
|
|
|
|
var charaDataToUpdate = characterData.CheckUpdatedData(applicationBase, _cachedData?.DeepClone() ?? new(), Logger, this,
|
|
|
|
|
var charaDataToUpdate = characterData.CheckUpdatedData(applicationBase, _lastAppliedData?.DeepClone() ?? new(), Logger, this,
|
|
|
|
|
forceApplyCustomization, _forceApplyMods, suppressForcedModRedraw);
|
|
|
|
|
|
|
|
|
|
if (handlerReady && _forceApplyMods)
|
|
|
|
|
@@ -1282,6 +1362,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
Guid applicationId = Guid.NewGuid();
|
|
|
|
|
_applicationCancellationTokenSource?.CancelDispose();
|
|
|
|
|
_applicationCancellationTokenSource = null;
|
|
|
|
|
LogDownloadCancellation("dispose");
|
|
|
|
|
_downloadCancellationTokenSource?.CancelDispose();
|
|
|
|
|
_downloadCancellationTokenSource = null;
|
|
|
|
|
ClearAllOwnedObjectRetries();
|
|
|
|
|
@@ -1363,6 +1444,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
{
|
|
|
|
|
PlayerName = null;
|
|
|
|
|
_cachedData = null;
|
|
|
|
|
_lastAppliedData = null;
|
|
|
|
|
LastReceivedCharacterData = null;
|
|
|
|
|
_performanceMetricsCache.Clear(Ident);
|
|
|
|
|
Logger.LogDebug("Disposing {name} complete", name);
|
|
|
|
|
@@ -1695,6 +1777,7 @@ 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));
|
|
|
|
|
|
|
|
|
|
LogDownloadCancellation("new download request", applicationBase);
|
|
|
|
|
_downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate() ?? new CancellationTokenSource();
|
|
|
|
|
var downloadToken = _downloadCancellationTokenSource.Token;
|
|
|
|
|
_ = DownloadAndApplyCharacterAsync(applicationBase, charaData, updatedData, updateModdedPaths, updateManip, downloadToken)
|
|
|
|
|
@@ -1741,13 +1824,14 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var handlerForDownload = _charaHandler;
|
|
|
|
|
_pairDownloadTask = Task.Run(async () => await _downloadManager.DownloadFiles(handlerForDownload, toDownloadReplacements, toDownloadFiles, downloadToken, skipDownscaleForPair, skipDecimationForPair).ConfigureAwait(false));
|
|
|
|
|
_pairDownloadTask = _downloadManager.DownloadFiles(handlerForDownload, toDownloadReplacements, toDownloadFiles, downloadToken, skipDownscaleForPair, skipDecimationForPair);
|
|
|
|
|
|
|
|
|
|
await _pairDownloadTask.ConfigureAwait(false);
|
|
|
|
|
|
|
|
|
|
if (downloadToken.IsCancellationRequested)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogTrace("[BASE-{appBase}] Detected cancellation", applicationBase);
|
|
|
|
|
_pendingModReapply = true;
|
|
|
|
|
RecordFailure("Download cancelled", "Cancellation");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
@@ -1809,6 +1893,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
if (handlerForApply is null || handlerForApply.Address == nint.Zero)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogDebug("[BASE-{appBase}] Handler not available for {player}, cached data for later application", applicationBase, GetLogIdentifier());
|
|
|
|
|
_pendingModReapply = true;
|
|
|
|
|
_cachedData = charaData;
|
|
|
|
|
_pairStateCache.Store(Ident, charaData);
|
|
|
|
|
RecordFailure("Handler not available for application", "HandlerUnavailable");
|
|
|
|
|
@@ -1836,6 +1921,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
|
|
|
|
|
_applicationTask = ApplyCharacterDataAsync(applicationBase, handlerForApply, charaData, updatedData, updateModdedPaths, updateManip, moddedPaths, token);
|
|
|
|
|
}
|
|
|
|
|
catch (OperationCanceledException) when (downloadToken.IsCancellationRequested)
|
|
|
|
|
{
|
|
|
|
|
_pendingModReapply = true;
|
|
|
|
|
RecordFailure("Download cancelled", "Cancellation");
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
await concurrencyLease.DisposeAsync().ConfigureAwait(false);
|
|
|
|
|
@@ -1859,6 +1949,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
{
|
|
|
|
|
Logger.LogDebug("[BASE-{applicationId}] Timed out waiting for {handler} to fully load, caching data for later application",
|
|
|
|
|
applicationBase, GetLogIdentifier());
|
|
|
|
|
_pendingModReapply = true;
|
|
|
|
|
_cachedData = charaData;
|
|
|
|
|
_pairStateCache.Store(Ident, charaData);
|
|
|
|
|
RecordFailure("Actor not fully loaded within timeout", "FullyLoadedTimeout");
|
|
|
|
|
@@ -1875,6 +1966,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
if (penumbraCollection == Guid.Empty)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogTrace("[BASE-{applicationId}] Penumbra collection unavailable for {handler}, caching data for later application", applicationBase, GetLogIdentifier());
|
|
|
|
|
_pendingModReapply = true;
|
|
|
|
|
_cachedData = charaData;
|
|
|
|
|
_pairStateCache.Store(Ident, charaData);
|
|
|
|
|
RecordFailure("Penumbra collection unavailable", "PenumbraUnavailable");
|
|
|
|
|
@@ -1893,6 +1985,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
if (!objIndex.HasValue)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogDebug("[BASE-{applicationId}] GameObject not available for {handler}, caching data for later application", applicationBase, GetLogIdentifier());
|
|
|
|
|
_pendingModReapply = true;
|
|
|
|
|
_cachedData = charaData;
|
|
|
|
|
_pairStateCache.Store(Ident, charaData);
|
|
|
|
|
RecordFailure("Game object not available for application", "GameObjectUnavailable");
|
|
|
|
|
@@ -1958,6 +2051,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_cachedData = charaData;
|
|
|
|
|
_lastAppliedData = charaData;
|
|
|
|
|
_pairStateCache.Store(Ident, charaData);
|
|
|
|
|
if (LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0)
|
|
|
|
|
{
|
|
|
|
|
@@ -1977,6 +2071,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
catch (OperationCanceledException)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogDebug("[{applicationId}] Application cancelled for {handler}", _applicationId, GetLogIdentifier());
|
|
|
|
|
_pendingModReapply = true;
|
|
|
|
|
_cachedData = charaData;
|
|
|
|
|
_pairStateCache.Store(Ident, charaData);
|
|
|
|
|
RecordFailure("Application cancelled", "Cancellation");
|
|
|
|
|
@@ -2072,6 +2167,25 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TryApplyQueuedData();
|
|
|
|
|
|
|
|
|
|
if (_pendingModReapply && IsVisible && !IsApplying && LastReceivedCharacterData is not null && CanApplyNow())
|
|
|
|
|
{
|
|
|
|
|
var now = DateTime.UtcNow;
|
|
|
|
|
if (!_lastApplyAttemptAt.HasValue || now - _lastApplyAttemptAt.Value > TimeSpan.FromSeconds(5))
|
|
|
|
|
{
|
|
|
|
|
_ = Task.Run(() =>
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
ApplyLastReceivedData(forced: true);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Logger.LogError(ex, "Failed to reapply pending data for {handler}", GetLogIdentifier());
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void HandleVisibilityLoss(bool logChange)
|
|
|
|
|
@@ -2079,6 +2193,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
IsVisible = false;
|
|
|
|
|
_charaHandler?.Invalidate();
|
|
|
|
|
ClearAllOwnedObjectRetries();
|
|
|
|
|
LogDownloadCancellation("visibility lost");
|
|
|
|
|
_downloadCancellationTokenSource?.CancelDispose();
|
|
|
|
|
_downloadCancellationTokenSource = null;
|
|
|
|
|
if (logChange)
|
|
|
|
|
@@ -2384,7 +2499,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
|
|
|
|
|
private async Task<bool> RevertToRestoredAsync(Guid applicationId)
|
|
|
|
|
{
|
|
|
|
|
if (_charaHandler is null || _charaHandler.Address == nint.Zero)
|
|
|
|
|
var handler = _charaHandler;
|
|
|
|
|
if (handler is null || handler.Address == nint.Zero)
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
@@ -2392,7 +2508,15 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var reverted = false;
|
|
|
|
|
var gameObject = await _dalamudUtil.RunOnFrameworkThread(() => _charaHandler.GetGameObject()).ConfigureAwait(false);
|
|
|
|
|
var gameObject = await _dalamudUtil.RunOnFrameworkThread(() =>
|
|
|
|
|
{
|
|
|
|
|
if (handler.Address == nint.Zero)
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return handler.GetGameObject();
|
|
|
|
|
}).ConfigureAwait(false);
|
|
|
|
|
if (gameObject is not Dalamud.Game.ClientState.Objects.Types.ICharacter character)
|
|
|
|
|
{
|
|
|
|
|
return false;
|
|
|
|
|
@@ -2450,6 +2574,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
|
|
|
|
|
private void DisableSync()
|
|
|
|
|
{
|
|
|
|
|
LogDownloadCancellation("sync disabled");
|
|
|
|
|
_downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate();
|
|
|
|
|
_applicationCancellationTokenSource = _applicationCancellationTokenSource?.CancelRecreate();
|
|
|
|
|
}
|
|
|
|
|
@@ -2457,6 +2582,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
|
|
|
|
private void EnableSync()
|
|
|
|
|
{
|
|
|
|
|
TryApplyQueuedData();
|
|
|
|
|
|
|
|
|
|
if (_pendingModReapply && LastReceivedCharacterData is not null && !IsApplying && CanApplyNow())
|
|
|
|
|
{
|
|
|
|
|
ApplyLastReceivedData(forced: true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void TryApplyQueuedData()
|
|
|
|
|
|