"improving" pair handler clean up and some other stuff
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using Dalamud.Plugin.Services;
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data.Enum;
|
||||
using LightlessSync.API.Data.Extensions;
|
||||
@@ -48,6 +49,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
private readonly PairPerformanceMetricsCache _performanceMetricsCache;
|
||||
private readonly PenumbraTempCollectionJanitor _tempCollectionJanitor;
|
||||
private readonly PairManager _pairManager;
|
||||
private readonly IFramework _framework;
|
||||
private CancellationTokenSource? _applicationCancellationTokenSource;
|
||||
private Guid _applicationId;
|
||||
private Task? _applicationTask;
|
||||
@@ -66,6 +68,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
private int _lastMissingNonCriticalMods;
|
||||
private int _lastMissingForbiddenMods;
|
||||
private bool _lastMissingCachedFiles;
|
||||
private string? _lastSuccessfulDataHash;
|
||||
private bool _isVisible;
|
||||
private Guid _penumbraCollection;
|
||||
private readonly object _collectionGate = new();
|
||||
@@ -82,6 +85,13 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
private readonly object _visibilityGraceGate = new();
|
||||
private CancellationTokenSource? _visibilityGraceCts;
|
||||
private static readonly TimeSpan VisibilityEvictionGrace = TimeSpan.FromMinutes(1);
|
||||
private readonly object _ownedRetryGate = new();
|
||||
private readonly Dictionary<ObjectKind, HashSet<PlayerChanges>> _pendingOwnedChanges = new();
|
||||
private CancellationTokenSource? _ownedRetryCts;
|
||||
private Task _ownedRetryTask = Task.CompletedTask;
|
||||
private static readonly TimeSpan OwnedRetryInitialDelay = TimeSpan.FromSeconds(1);
|
||||
private static readonly TimeSpan OwnedRetryMaxDelay = TimeSpan.FromSeconds(10);
|
||||
private static readonly TimeSpan OwnedRetryStaleDataGrace = TimeSpan.FromMinutes(5);
|
||||
private static readonly HashSet<string> NonPriorityModExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
".tmb",
|
||||
@@ -95,10 +105,15 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
private DateTime _nextActorLookupUtc = DateTime.MinValue;
|
||||
private static readonly TimeSpan ActorLookupInterval = TimeSpan.FromSeconds(1);
|
||||
private static readonly SemaphoreSlim ActorInitializationLimiter = new(1, 1);
|
||||
private const int FullyLoadedTimeoutMsPlayer = 30000;
|
||||
private const int FullyLoadedTimeoutMsOther = 5000;
|
||||
private readonly object _actorInitializationGate = new();
|
||||
private ActorObjectService.ActorDescriptor? _pendingActorDescriptor;
|
||||
private bool _actorInitializationInProgress;
|
||||
private bool _frameworkUpdateSubscribed;
|
||||
private nint _lastKnownAddress = nint.Zero;
|
||||
private ushort _lastKnownObjectIndex = ushort.MaxValue;
|
||||
private string? _lastKnownName;
|
||||
|
||||
public DateTime? InvisibleSinceUtc => _invisibleSinceUtc;
|
||||
public DateTime? VisibilityEvictionDueAtUtc => _visibilityEvictionDueAtUtc;
|
||||
@@ -175,6 +190,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
FileDownloadManager transferManager,
|
||||
PluginWarningNotificationService pluginWarningNotificationManager,
|
||||
DalamudUtilService dalamudUtil,
|
||||
IFramework framework,
|
||||
ActorObjectService actorObjectService,
|
||||
IHostApplicationLifetime lifetime,
|
||||
FileCacheManager fileDbManager,
|
||||
@@ -193,6 +209,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
_downloadManager = transferManager;
|
||||
_pluginWarningNotificationManager = pluginWarningNotificationManager;
|
||||
_dalamudUtil = dalamudUtil;
|
||||
_framework = framework;
|
||||
_actorObjectService = actorObjectService;
|
||||
_lifetime = lifetime;
|
||||
_fileDbManager = fileDbManager;
|
||||
@@ -432,7 +449,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetPenumbraCollection(bool releaseFromPenumbra = true, string? reason = null)
|
||||
private void ResetPenumbraCollection(bool releaseFromPenumbra = true, string? reason = null, bool awaitIpc = true)
|
||||
{
|
||||
Guid toRelease = Guid.Empty;
|
||||
bool hadCollection = false;
|
||||
@@ -466,16 +483,33 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
var applicationId = Guid.NewGuid();
|
||||
if (awaitIpc)
|
||||
{
|
||||
var applicationId = Guid.NewGuid();
|
||||
Logger.LogTrace("[{applicationId}] Removing temp collection {CollectionId} for {handler} ({reason})", applicationId, toRelease, GetLogIdentifier(), reason ?? "Cleanup");
|
||||
_ipcManager.Penumbra.RemoveTemporaryCollectionAsync(Logger, applicationId, toRelease).GetAwaiter().GetResult();
|
||||
try
|
||||
{
|
||||
Logger.LogTrace("[{applicationId}] Removing temp collection {CollectionId} for {handler} ({reason})", applicationId, toRelease, GetLogIdentifier(), reason ?? "Cleanup");
|
||||
_ipcManager.Penumbra.RemoveTemporaryCollectionAsync(Logger, applicationId, toRelease).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogDebug(ex, "Failed to remove temporary Penumbra collection for {handler}", GetLogIdentifier());
|
||||
}
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
Logger.LogDebug(ex, "Failed to remove temporary Penumbra collection for {handler}", GetLogIdentifier());
|
||||
}
|
||||
try
|
||||
{
|
||||
Logger.LogTrace("[{applicationId}] Removing temp collection {CollectionId} for {handler} ({reason})", applicationId, toRelease, GetLogIdentifier(), reason ?? "Cleanup");
|
||||
await _ipcManager.Penumbra.RemoveTemporaryCollectionAsync(Logger, applicationId, toRelease).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogDebug(ex, "Failed to remove temporary Penumbra collection for {handler}", GetLogIdentifier());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private bool AnyPair(Func<PairConnection, bool> predicate)
|
||||
@@ -559,9 +593,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
}
|
||||
|
||||
var hasMissingCachedFiles = HasMissingCachedFiles(LastReceivedCharacterData);
|
||||
var missingStarted = !_lastMissingCachedFiles && hasMissingCachedFiles;
|
||||
var missingResolved = _lastMissingCachedFiles && !hasMissingCachedFiles;
|
||||
_lastMissingCachedFiles = hasMissingCachedFiles;
|
||||
var shouldForce = forced || missingResolved;
|
||||
var shouldForce = forced || missingStarted || missingResolved;
|
||||
var forceApplyCustomization = forced;
|
||||
|
||||
if (IsPaused())
|
||||
{
|
||||
@@ -569,7 +605,22 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldForce)
|
||||
var sanitized = CloneAndSanitizeLastReceived(out var dataHash);
|
||||
if (sanitized is null)
|
||||
{
|
||||
Logger.LogTrace("Sanitized data null for {Ident}", Ident);
|
||||
return;
|
||||
}
|
||||
var dataApplied = !string.IsNullOrEmpty(dataHash)
|
||||
&& string.Equals(dataHash, _lastSuccessfulDataHash ?? string.Empty, StringComparison.Ordinal);
|
||||
var needsApply = !dataApplied;
|
||||
var hasModReplacements = sanitized.FileReplacements.Values.Any(list => list.Count > 0);
|
||||
var needsModReapply = needsApply && hasModReplacements;
|
||||
var shouldForceMods = shouldForce || needsModReapply;
|
||||
forceApplyCustomization = forced || needsApply;
|
||||
var suppressForcedModRedraw = !forced && hasMissingCachedFiles && dataApplied;
|
||||
|
||||
if (shouldForceMods)
|
||||
{
|
||||
_forceApplyMods = true;
|
||||
_forceFullReapply = true;
|
||||
@@ -579,15 +630,21 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
LastAppliedApproximateEffectiveVRAMBytes = -1;
|
||||
}
|
||||
|
||||
var sanitized = CloneAndSanitizeLastReceived(out _);
|
||||
if (sanitized is null)
|
||||
{
|
||||
Logger.LogTrace("Sanitized data null for {Ident}", Ident);
|
||||
return;
|
||||
}
|
||||
|
||||
_pairStateCache.Store(Ident, sanitized);
|
||||
|
||||
if (!IsVisible && !_pauseRequested)
|
||||
{
|
||||
if (_charaHandler is not null && _charaHandler.Address == nint.Zero)
|
||||
{
|
||||
_charaHandler.Refresh();
|
||||
}
|
||||
|
||||
if (PlayerCharacter != nint.Zero)
|
||||
{
|
||||
IsVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!IsVisible)
|
||||
{
|
||||
Logger.LogTrace("Handler for {Ident} not visible, caching sanitized data for later", Ident);
|
||||
@@ -596,7 +653,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyCharacterData(Guid.NewGuid(), sanitized, shouldForce);
|
||||
ApplyCharacterData(Guid.NewGuid(), sanitized, forceApplyCustomization, suppressForcedModRedraw);
|
||||
}
|
||||
|
||||
public bool FetchPerformanceMetricsFromCache()
|
||||
@@ -906,7 +963,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
SetUploading(false);
|
||||
}
|
||||
|
||||
public void ApplyCharacterData(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization = false)
|
||||
public void ApplyCharacterData(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization = false, bool suppressForcedModRedraw = false)
|
||||
{
|
||||
_lastApplyAttemptAt = DateTime.UtcNow;
|
||||
ClearFailureState();
|
||||
@@ -1000,7 +1057,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Informational,
|
||||
"Applying Character Data")));
|
||||
|
||||
var charaDataToUpdate = characterData.CheckUpdatedData(applicationBase, _cachedData?.DeepClone() ?? new(), Logger, this, forceApplyCustomization, _forceApplyMods);
|
||||
var charaDataToUpdate = characterData.CheckUpdatedData(applicationBase, _cachedData?.DeepClone() ?? new(), Logger, this,
|
||||
forceApplyCustomization, _forceApplyMods, suppressForcedModRedraw);
|
||||
|
||||
if (handlerReady && _forceApplyMods)
|
||||
{
|
||||
@@ -1097,12 +1155,183 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
}, CancellationToken.None);
|
||||
}
|
||||
|
||||
private void ScheduleOwnedObjectRetry(ObjectKind kind, HashSet<PlayerChanges> changes)
|
||||
{
|
||||
if (kind == ObjectKind.Player || changes.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_ownedRetryGate)
|
||||
{
|
||||
_pendingOwnedChanges[kind] = new HashSet<PlayerChanges>(changes);
|
||||
if (!_ownedRetryTask.IsCompleted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ownedRetryCts = _ownedRetryCts?.CancelRecreate() ?? new CancellationTokenSource();
|
||||
var token = _ownedRetryCts.Token;
|
||||
_ownedRetryTask = Task.Run(() => OwnedObjectRetryLoopAsync(token), CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearOwnedObjectRetry(ObjectKind kind)
|
||||
{
|
||||
lock (_ownedRetryGate)
|
||||
{
|
||||
if (!_pendingOwnedChanges.Remove(kind))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearAllOwnedObjectRetries()
|
||||
{
|
||||
lock (_ownedRetryGate)
|
||||
{
|
||||
_pendingOwnedChanges.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsOwnedRetryDataStale()
|
||||
{
|
||||
if (!_lastDataReceivedAt.HasValue)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return DateTime.UtcNow - _lastDataReceivedAt.Value > OwnedRetryStaleDataGrace;
|
||||
}
|
||||
|
||||
private async Task OwnedObjectRetryLoopAsync(CancellationToken token)
|
||||
{
|
||||
var delay = OwnedRetryInitialDelay;
|
||||
try
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
if (IsOwnedRetryDataStale())
|
||||
{
|
||||
ClearAllOwnedObjectRetries();
|
||||
return;
|
||||
}
|
||||
|
||||
Dictionary<ObjectKind, HashSet<PlayerChanges>> pending;
|
||||
lock (_ownedRetryGate)
|
||||
{
|
||||
if (_pendingOwnedChanges.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
pending = _pendingOwnedChanges.ToDictionary(kvp => kvp.Key, kvp => new HashSet<PlayerChanges>(kvp.Value));
|
||||
}
|
||||
|
||||
if (!IsVisible || IsPaused() || !CanApplyNow() || PlayerCharacter == nint.Zero || _charaHandler is null)
|
||||
{
|
||||
await Task.Delay(delay, token).ConfigureAwait(false);
|
||||
delay = IncreaseRetryDelay(delay);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((_applicationTask?.IsCompleted ?? true) == false || (_pairDownloadTask?.IsCompleted ?? true) == false)
|
||||
{
|
||||
await Task.Delay(delay, token).ConfigureAwait(false);
|
||||
delay = IncreaseRetryDelay(delay);
|
||||
continue;
|
||||
}
|
||||
|
||||
var sanitized = CloneAndSanitizeLastReceived(out _);
|
||||
if (sanitized is null)
|
||||
{
|
||||
await Task.Delay(delay, token).ConfigureAwait(false);
|
||||
delay = IncreaseRetryDelay(delay);
|
||||
continue;
|
||||
}
|
||||
|
||||
bool anyApplied = false;
|
||||
foreach (var entry in pending)
|
||||
{
|
||||
if (!HasAppearanceDataForKind(sanitized, entry.Key))
|
||||
{
|
||||
ClearOwnedObjectRetry(entry.Key);
|
||||
continue;
|
||||
}
|
||||
|
||||
var applied = await ApplyCustomizationDataAsync(Guid.NewGuid(), entry, sanitized, token).ConfigureAwait(false);
|
||||
if (applied)
|
||||
{
|
||||
ClearOwnedObjectRetry(entry.Key);
|
||||
anyApplied = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!anyApplied)
|
||||
{
|
||||
await Task.Delay(delay, token).ConfigureAwait(false);
|
||||
delay = IncreaseRetryDelay(delay);
|
||||
}
|
||||
else
|
||||
{
|
||||
delay = OwnedRetryInitialDelay;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogDebug(ex, "Owned object retry task failed for {handler}", GetLogIdentifier());
|
||||
}
|
||||
}
|
||||
|
||||
private static TimeSpan IncreaseRetryDelay(TimeSpan delay)
|
||||
{
|
||||
var nextMs = Math.Min(delay.TotalMilliseconds * 2, OwnedRetryMaxDelay.TotalMilliseconds);
|
||||
return TimeSpan.FromMilliseconds(nextMs);
|
||||
}
|
||||
|
||||
private static bool HasAppearanceDataForKind(CharacterData data, ObjectKind kind)
|
||||
{
|
||||
if (data.FileReplacements.TryGetValue(kind, out var replacements) && replacements.Count > 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (data.GlamourerData.TryGetValue(kind, out var glamourer) && !string.IsNullOrEmpty(glamourer))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (data.CustomizePlusData.TryGetValue(kind, out var customize) && !string.IsNullOrEmpty(customize))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
SetUploading(false);
|
||||
var name = PlayerName;
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
_lastKnownName = name;
|
||||
}
|
||||
|
||||
var currentAddress = PlayerCharacter;
|
||||
if (currentAddress != nint.Zero)
|
||||
{
|
||||
_lastKnownAddress = currentAddress;
|
||||
}
|
||||
|
||||
var user = GetPrimaryUserDataSafe();
|
||||
var alias = GetPrimaryAliasOrUidSafe();
|
||||
Logger.LogDebug("Disposing {name} ({user})", name, alias);
|
||||
@@ -1113,6 +1342,9 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
_applicationCancellationTokenSource = null;
|
||||
_downloadCancellationTokenSource?.CancelDispose();
|
||||
_downloadCancellationTokenSource = null;
|
||||
ClearAllOwnedObjectRetries();
|
||||
_ownedRetryCts?.CancelDispose();
|
||||
_ownedRetryCts = null;
|
||||
_downloadManager.Dispose();
|
||||
_charaHandler?.Dispose();
|
||||
CancelVisibilityGraceTask();
|
||||
@@ -1125,43 +1357,62 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
Mediator.Publish(new EventMessage(new Event(name, user, nameof(PairHandlerAdapter), EventSeverity.Informational, "Disposing User")));
|
||||
}
|
||||
|
||||
if (_lifetime.ApplicationStopping.IsCancellationRequested) return;
|
||||
|
||||
if (_dalamudUtil is { IsZoning: false, IsInCutscene: false } && !string.IsNullOrEmpty(name))
|
||||
if (IsFrameworkUnloading())
|
||||
{
|
||||
Logger.LogTrace("[{applicationId}] Restoring state for {name} ({user})", applicationId, name, alias);
|
||||
Logger.LogDebug("[{applicationId}] Removing Temp Collection for {name} ({user})", applicationId, name, alias);
|
||||
ResetPenumbraCollection(reason: nameof(Dispose));
|
||||
if (!IsVisible)
|
||||
Logger.LogWarning("Framework is unloading, skipping disposal for {name} ({user})", name, alias);
|
||||
return;
|
||||
}
|
||||
|
||||
var isStopping = _lifetime.ApplicationStopping.IsCancellationRequested;
|
||||
if (isStopping)
|
||||
{
|
||||
ResetPenumbraCollection(reason: "DisposeStopping", awaitIpc: false);
|
||||
ScheduleSafeRevertOnDisposal(applicationId, name, alias);
|
||||
return;
|
||||
}
|
||||
|
||||
var canCleanup = !string.IsNullOrEmpty(name)
|
||||
&& _dalamudUtil.IsLoggedIn
|
||||
&& !_dalamudUtil.IsZoning
|
||||
&& !_dalamudUtil.IsInCutscene;
|
||||
|
||||
if (!canCleanup)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogTrace("[{applicationId}] Restoring state for {name} ({user})", applicationId, name, alias);
|
||||
Logger.LogDebug("[{applicationId}] Removing Temp Collection for {name} ({user})", applicationId, name, alias);
|
||||
ResetPenumbraCollection(reason: nameof(Dispose));
|
||||
if (!IsVisible)
|
||||
{
|
||||
Logger.LogDebug("[{applicationId}] Restoring Glamourer for {name} ({user})", applicationId, name, alias);
|
||||
_ipcManager.Glamourer.RevertByNameAsync(Logger, name, applicationId).GetAwaiter().GetResult();
|
||||
}
|
||||
else
|
||||
{
|
||||
using var cts = new CancellationTokenSource();
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(60));
|
||||
|
||||
var effectiveCachedData = _cachedData ?? _pairStateCache.TryLoad(Ident);
|
||||
if (effectiveCachedData is not null)
|
||||
{
|
||||
Logger.LogDebug("[{applicationId}] Restoring Glamourer for {name} ({user})", applicationId, name, alias);
|
||||
_ipcManager.Glamourer.RevertByNameAsync(Logger, name, applicationId).GetAwaiter().GetResult();
|
||||
_cachedData = effectiveCachedData;
|
||||
}
|
||||
else
|
||||
|
||||
Logger.LogInformation("[{applicationId}] CachedData is null {isNull}, contains things: {contains}",
|
||||
applicationId, _cachedData == null, _cachedData?.FileReplacements.Any() ?? false);
|
||||
|
||||
foreach (KeyValuePair<ObjectKind, List<FileReplacementData>> item in _cachedData?.FileReplacements ?? [])
|
||||
{
|
||||
using var cts = new CancellationTokenSource();
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(60));
|
||||
|
||||
var effectiveCachedData = _cachedData ?? _pairStateCache.TryLoad(Ident);
|
||||
if (effectiveCachedData is not null)
|
||||
try
|
||||
{
|
||||
_cachedData = effectiveCachedData;
|
||||
RevertCustomizationDataAsync(item.Key, name, applicationId, cts.Token).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
Logger.LogInformation("[{applicationId}] CachedData is null {isNull}, contains things: {contains}",
|
||||
applicationId, _cachedData == null, _cachedData?.FileReplacements.Any() ?? false);
|
||||
|
||||
foreach (KeyValuePair<ObjectKind, List<FileReplacementData>> item in _cachedData?.FileReplacements ?? [])
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
RevertCustomizationDataAsync(item.Key, name, applicationId, cts.Token).GetAwaiter().GetResult();
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
Logger.LogWarning(ex, "Failed disposing player (not present anymore?)");
|
||||
break;
|
||||
}
|
||||
Logger.LogWarning(ex, "Failed disposing player (not present anymore?)");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1174,6 +1425,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
{
|
||||
PlayerName = null;
|
||||
_cachedData = null;
|
||||
_lastSuccessfulDataHash = null;
|
||||
_lastAppliedModdedPaths = null;
|
||||
_needsCollectionRebuild = false;
|
||||
_performanceMetricsCache.Clear(Ident);
|
||||
@@ -1181,9 +1433,145 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ApplyCustomizationDataAsync(Guid applicationId, KeyValuePair<ObjectKind, HashSet<PlayerChanges>> changes, CharacterData charaData, CancellationToken token)
|
||||
private bool IsFrameworkUnloading()
|
||||
{
|
||||
if (PlayerCharacter == nint.Zero) return;
|
||||
try
|
||||
{
|
||||
var prop = _framework.GetType().GetProperty("IsFrameworkUnloading");
|
||||
if (prop?.PropertyType == typeof(bool))
|
||||
{
|
||||
return (bool)prop.GetValue(_framework)!;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ScheduleSafeRevertOnDisposal(Guid applicationId, string? name, string alias)
|
||||
{
|
||||
var cleanupName = !string.IsNullOrEmpty(name) ? name : _lastKnownName;
|
||||
var cleanupAddress = _lastKnownAddress != nint.Zero
|
||||
? _lastKnownAddress
|
||||
: _dalamudUtil.GetPlayerCharacterFromCachedTableByIdent(Ident);
|
||||
var cleanupObjectIndex = _lastKnownObjectIndex;
|
||||
var cleanupIdent = Ident;
|
||||
var customizeIds = _customizeIds.Values.Where(id => id.HasValue)
|
||||
.Select(id => id!.Value)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
if (string.IsNullOrEmpty(cleanupName)
|
||||
&& cleanupAddress == nint.Zero
|
||||
&& cleanupObjectIndex == ushort.MaxValue
|
||||
&& customizeIds.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ = Task.Run(() => SafeRevertOnDisposalAsync(
|
||||
applicationId,
|
||||
cleanupName,
|
||||
cleanupAddress,
|
||||
cleanupObjectIndex,
|
||||
cleanupIdent,
|
||||
customizeIds,
|
||||
alias));
|
||||
}
|
||||
|
||||
private async Task SafeRevertOnDisposalAsync(
|
||||
Guid applicationId,
|
||||
string? cleanupName,
|
||||
nint cleanupAddress,
|
||||
ushort cleanupObjectIndex,
|
||||
string cleanupIdent,
|
||||
IReadOnlyList<Guid> customizeIds,
|
||||
string alias)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsFrameworkUnloading())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(cleanupName) && _ipcManager.Glamourer.APIAvailable)
|
||||
{
|
||||
Logger.LogDebug("[{applicationId}] Restoring Glamourer for {name} ({user})", applicationId, cleanupName, alias);
|
||||
await _ipcManager.Glamourer.RevertByNameAsync(Logger, cleanupName, applicationId).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (_ipcManager.CustomizePlus.APIAvailable && customizeIds.Count > 0)
|
||||
{
|
||||
foreach (var customizeId in customizeIds)
|
||||
{
|
||||
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
var address = cleanupAddress;
|
||||
if (address == nint.Zero && cleanupObjectIndex != ushort.MaxValue)
|
||||
{
|
||||
address = await _dalamudUtil.RunOnFrameworkThread(() =>
|
||||
{
|
||||
var obj = _dalamudUtil.GetCharacterFromObjectTableByIndex(cleanupObjectIndex);
|
||||
if (obj is not Dalamud.Game.ClientState.Objects.SubKinds.IPlayerCharacter player)
|
||||
{
|
||||
return nint.Zero;
|
||||
}
|
||||
|
||||
if (!DalamudUtilService.TryGetHashedCID(player, out var hash)
|
||||
|| !string.Equals(hash, cleanupIdent, StringComparison.Ordinal))
|
||||
{
|
||||
return nint.Zero;
|
||||
}
|
||||
|
||||
return player.Address;
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (address == nint.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_ipcManager.CustomizePlus.APIAvailable)
|
||||
{
|
||||
await _ipcManager.CustomizePlus.RevertAsync(address).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (_ipcManager.Heels.APIAvailable)
|
||||
{
|
||||
await _ipcManager.Heels.RestoreOffsetForPlayerAsync(address).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (_ipcManager.Honorific.APIAvailable)
|
||||
{
|
||||
await _ipcManager.Honorific.ClearTitleAsync(address).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (_ipcManager.Moodles.APIAvailable)
|
||||
{
|
||||
await _ipcManager.Moodles.RevertStatusAsync(address).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (_ipcManager.PetNames.APIAvailable)
|
||||
{
|
||||
await _ipcManager.PetNames.ClearPlayerData(address).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogDebug(ex, "Failed shutdown cleanup for {name}", cleanupName ?? cleanupIdent);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> ApplyCustomizationDataAsync(Guid applicationId, KeyValuePair<ObjectKind, HashSet<PlayerChanges>> changes, CharacterData charaData, CancellationToken token)
|
||||
{
|
||||
if (PlayerCharacter == nint.Zero) return false;
|
||||
var ptr = PlayerCharacter;
|
||||
|
||||
var handler = changes.Key switch
|
||||
@@ -1199,14 +1587,29 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
{
|
||||
if (handler.Address == nint.Zero)
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger.LogDebug("[{applicationId}] Applying Customization Data for {handler}", applicationId, handler);
|
||||
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handler, applicationId, 30000, token).ConfigureAwait(false);
|
||||
await handler.IsBeingDrawnRunOnFrameworkAsync().ConfigureAwait(false);
|
||||
if (handler.ObjectKind != ObjectKind.Player
|
||||
&& handler.CurrentDrawCondition == GameObjectHandler.DrawCondition.DrawObjectZero)
|
||||
{
|
||||
Logger.LogDebug("[{applicationId}] Skipping customization apply for {handler}, draw object not available", applicationId, handler);
|
||||
return false;
|
||||
}
|
||||
|
||||
var drawTimeoutMs = handler.ObjectKind == ObjectKind.Player ? 30000 : 5000;
|
||||
var fullyLoadedTimeoutMs = handler.ObjectKind == ObjectKind.Player ? FullyLoadedTimeoutMsPlayer : FullyLoadedTimeoutMsOther;
|
||||
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handler, applicationId, drawTimeoutMs, token).ConfigureAwait(false);
|
||||
if (handler.Address != nint.Zero)
|
||||
{
|
||||
await _actorObjectService.WaitForFullyLoadedAsync(handler.Address, token).ConfigureAwait(false);
|
||||
var fullyLoaded = await _actorObjectService.WaitForFullyLoadedAsync(handler.Address, token, fullyLoadedTimeoutMs).ConfigureAwait(false);
|
||||
if (!fullyLoaded)
|
||||
{
|
||||
Logger.LogDebug("[{applicationId}] Timed out waiting for {handler} to fully load, skipping customization apply", applicationId, handler);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
@@ -1270,6 +1673,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
{
|
||||
await _ipcManager.Penumbra.RedrawAsync(Logger, handler, applicationId, token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -1577,37 +1982,25 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
RecordFailure("Handler not available for application", "HandlerUnavailable");
|
||||
return;
|
||||
}
|
||||
_applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource();
|
||||
|
||||
if (_applicationTask != null && !_applicationTask.IsCompleted)
|
||||
var appToken = _applicationCancellationTokenSource?.Token;
|
||||
while ((!_applicationTask?.IsCompleted ?? false)
|
||||
&& !downloadToken.IsCancellationRequested
|
||||
&& (!appToken?.IsCancellationRequested ?? false))
|
||||
{
|
||||
Logger.LogDebug("[BASE-{appBase}] Cancelling current data application (Id: {id}) for player ({handler})", applicationBase, _applicationId, PlayerName);
|
||||
|
||||
var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||
var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(downloadToken, timeoutCts.Token);
|
||||
|
||||
try
|
||||
{
|
||||
await _applicationTask.WaitAsync(combinedCts.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Logger.LogWarning("[BASE-{appBase}] Timeout waiting for application task {id} to complete, proceeding anyway", applicationBase, _applicationId);
|
||||
}
|
||||
finally
|
||||
{
|
||||
timeoutCts.Dispose();
|
||||
combinedCts.Dispose();
|
||||
}
|
||||
Logger.LogDebug("[BASE-{appBase}] Waiting for current data application (Id: {id}) for player ({handler}) to finish",
|
||||
applicationBase, _applicationId, PlayerName);
|
||||
await Task.Delay(250).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (downloadToken.IsCancellationRequested)
|
||||
if (downloadToken.IsCancellationRequested || (appToken?.IsCancellationRequested ?? false))
|
||||
{
|
||||
_forceFullReapply = true;
|
||||
RecordFailure("Application cancelled", "Cancellation");
|
||||
return;
|
||||
}
|
||||
|
||||
_applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource();
|
||||
var token = _applicationCancellationTokenSource.Token;
|
||||
|
||||
_applicationTask = ApplyCharacterDataAsync(applicationBase, handlerForApply, charaData, updatedData, updateModdedPaths, updateManip, moddedPaths, wantsModApply, pendingModReapply, token);
|
||||
@@ -1630,7 +2023,17 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handlerForApply, _applicationId, 30000, token).ConfigureAwait(false);
|
||||
if (handlerForApply.Address != nint.Zero)
|
||||
{
|
||||
await _actorObjectService.WaitForFullyLoadedAsync(handlerForApply.Address, token).ConfigureAwait(false);
|
||||
var fullyLoaded = await _actorObjectService.WaitForFullyLoadedAsync(handlerForApply.Address, token, FullyLoadedTimeoutMsPlayer).ConfigureAwait(false);
|
||||
if (!fullyLoaded)
|
||||
{
|
||||
Logger.LogDebug("[BASE-{applicationId}] Timed out waiting for {handler} to fully load, caching data for later application",
|
||||
applicationBase, GetLogIdentifier());
|
||||
_cachedData = charaData;
|
||||
_pairStateCache.Store(Ident, charaData);
|
||||
_forceFullReapply = true;
|
||||
RecordFailure("Actor not fully loaded within timeout", "FullyLoadedTimeout");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
@@ -1692,7 +2095,15 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
|
||||
foreach (var kind in updatedData)
|
||||
{
|
||||
await ApplyCustomizationDataAsync(_applicationId, kind, charaData, token).ConfigureAwait(false);
|
||||
var applied = await ApplyCustomizationDataAsync(_applicationId, kind, charaData, token).ConfigureAwait(false);
|
||||
if (applied)
|
||||
{
|
||||
ClearOwnedObjectRetry(kind.Key);
|
||||
}
|
||||
else if (kind.Key != ObjectKind.Player)
|
||||
{
|
||||
ScheduleOwnedObjectRetry(kind.Key, kind.Value);
|
||||
}
|
||||
token.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
@@ -1714,6 +2125,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
}
|
||||
|
||||
StorePerformanceMetrics(charaData);
|
||||
_lastSuccessfulDataHash = GetDataHashSafe(charaData);
|
||||
_lastSuccessfulApplyAt = DateTime.UtcNow;
|
||||
ClearFailureState();
|
||||
Logger.LogDebug("[{applicationId}] Application finished", _applicationId);
|
||||
@@ -1827,6 +2239,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
{
|
||||
IsVisible = false;
|
||||
_charaHandler?.Invalidate();
|
||||
ClearAllOwnedObjectRetries();
|
||||
_downloadCancellationTokenSource?.CancelDispose();
|
||||
_downloadCancellationTokenSource = null;
|
||||
if (logChange)
|
||||
@@ -1839,6 +2252,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
{
|
||||
PlayerName = name;
|
||||
_charaHandler = _gameObjectHandlerFactory.Create(ObjectKind.Player, () => _dalamudUtil.GetPlayerCharacterFromCachedTableByIdent(Ident), isWatched: false).GetAwaiter().GetResult();
|
||||
UpdateLastKnownActor(_charaHandler.Address, name);
|
||||
|
||||
var user = GetPrimaryUserData();
|
||||
if (!string.IsNullOrEmpty(user.UID))
|
||||
@@ -2185,6 +2599,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
if (descriptor.Address == nint.Zero)
|
||||
return;
|
||||
|
||||
UpdateLastKnownActor(descriptor);
|
||||
RefreshTrackedHandler(descriptor);
|
||||
QueueActorInitialization(descriptor);
|
||||
}
|
||||
@@ -2308,6 +2723,29 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
return !string.IsNullOrEmpty(hashedCid);
|
||||
}
|
||||
|
||||
private void UpdateLastKnownActor(ActorObjectService.ActorDescriptor descriptor)
|
||||
{
|
||||
_lastKnownAddress = descriptor.Address;
|
||||
_lastKnownObjectIndex = descriptor.ObjectIndex;
|
||||
if (!string.IsNullOrEmpty(descriptor.Name))
|
||||
{
|
||||
_lastKnownName = descriptor.Name;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateLastKnownActor(nint address, string? name)
|
||||
{
|
||||
if (address != nint.Zero)
|
||||
{
|
||||
_lastKnownAddress = address;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
_lastKnownName = name;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ApplyCustomizeAsync(nint address, string customizeData, ObjectKind kind)
|
||||
{
|
||||
_customizeIds[kind] = await _ipcManager.CustomizePlus.SetBodyScaleAsync(address, customizeData).ConfigureAwait(false);
|
||||
|
||||
Reference in New Issue
Block a user