2.0.0 #92

Merged
defnotken merged 171 commits from 2.0.0 into master 2025-12-21 17:19:36 +00:00
2 changed files with 114 additions and 430 deletions
Showing only changes of commit 28d9110cb0 - Show all commits

View File

@@ -70,8 +70,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private readonly TextureDownscaleService _textureDownscaleService; private readonly TextureDownscaleService _textureDownscaleService;
private readonly PairStateCache _pairStateCache; private readonly PairStateCache _pairStateCache;
private readonly PairManager _pairManager; private readonly PairManager _pairManager;
private Guid _currentDownloadOwnerToken;
private bool _downloadInProgress;
private CancellationTokenSource? _applicationCancellationTokenSource = new(); private CancellationTokenSource? _applicationCancellationTokenSource = new();
private Guid _applicationId; private Guid _applicationId;
private Task? _applicationTask; private Task? _applicationTask;
@@ -81,7 +79,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private CombatData? _dataReceivedInDowntime; private CombatData? _dataReceivedInDowntime;
private CancellationTokenSource? _downloadCancellationTokenSource = new(); private CancellationTokenSource? _downloadCancellationTokenSource = new();
private bool _forceApplyMods = false; private bool _forceApplyMods = false;
private bool _forceFullReapply;
private bool _isVisible; private bool _isVisible;
private Guid _penumbraCollection; private Guid _penumbraCollection;
private readonly object _collectionGate = new(); private readonly object _collectionGate = new();
@@ -90,7 +87,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private readonly object _pauseLock = new(); private readonly object _pauseLock = new();
private Task _pauseTransitionTask = Task.CompletedTask; private Task _pauseTransitionTask = Task.CompletedTask;
private bool _pauseRequested; private bool _pauseRequested;
private int _restoreRequested;
public string Ident { get; } public string Ident { get; }
public bool Initialized { get; private set; } public bool Initialized { get; private set; }
@@ -106,7 +102,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
_isVisible = value; _isVisible = value;
if (!_isVisible) if (!_isVisible)
{ {
ResetRestoreState();
DisableSync(); DisableSync();
ResetPenumbraCollection(reason: "VisibilityLost"); ResetPenumbraCollection(reason: "VisibilityLost");
} }
@@ -226,19 +221,14 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
Mediator.Subscribe<CutsceneEndMessage>(this, _ => EnableSync()); Mediator.Subscribe<CutsceneEndMessage>(this, _ => EnableSync());
Mediator.Subscribe<GposeStartMessage>(this, _ => DisableSync()); Mediator.Subscribe<GposeStartMessage>(this, _ => DisableSync());
Mediator.Subscribe<GposeEndMessage>(this, _ => EnableSync()); Mediator.Subscribe<GposeEndMessage>(this, _ => EnableSync());
Mediator.Subscribe<DownloadFinishedMessage>(this, msg => Mediator.Subscribe<DownloadFinishedMessage>(this, msg =>
{
if (_charaHandler is null || !ReferenceEquals(msg.DownloadId, _charaHandler))
{ {
if (_charaHandler is null || !ReferenceEquals(msg.DownloadId, _charaHandler)) return;
{ }
return; TryApplyQueuedData();
} });
if (_downloadManager.CurrentOwnerToken.HasValue
&& _downloadManager.CurrentOwnerToken == _currentDownloadOwnerToken)
{
TryApplyQueuedData();
}
});
Initialized = true; Initialized = true;
} }
@@ -446,7 +436,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
{ {
EnsureInitialized(); EnsureInitialized();
LastReceivedCharacterData = data; LastReceivedCharacterData = data;
ResetRestoreState();
ApplyLastReceivedData(); ApplyLastReceivedData();
} }
@@ -458,10 +447,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
} }
LastReceivedCharacterData = data; LastReceivedCharacterData = data;
ResetRestoreState();
_cachedData = null; _cachedData = null;
_forceApplyMods = true; _forceApplyMods = true;
_forceFullReapply = true;
LastAppliedDataBytes = -1; LastAppliedDataBytes = -1;
LastAppliedDataTris = -1; LastAppliedDataTris = -1;
LastAppliedApproximateVRAMBytes = -1; LastAppliedApproximateVRAMBytes = -1;
@@ -474,10 +461,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
if (LastReceivedCharacterData is null) if (LastReceivedCharacterData is null)
{ {
Logger.LogTrace("No cached data to apply for {Ident}", Ident); Logger.LogTrace("No cached data to apply for {Ident}", Ident);
if (forced)
{
EnsureRestoredStateWhileWaitingForData("ForcedReapplyWithoutCache", skipIfAlreadyRestored: false);
}
return; return;
} }
@@ -493,7 +476,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
{ {
_forceApplyMods = true; _forceApplyMods = true;
_cachedData = null; _cachedData = null;
_forceFullReapply = true;
LastAppliedDataBytes = -1; LastAppliedDataBytes = -1;
LastAppliedDataTris = -1; LastAppliedDataTris = -1;
LastAppliedApproximateVRAMBytes = -1; LastAppliedApproximateVRAMBytes = -1;
@@ -513,7 +495,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
{ {
Logger.LogTrace("Handler for {Ident} not visible, caching sanitized data for later", Ident); Logger.LogTrace("Handler for {Ident} not visible, caching sanitized data for later", Ident);
_cachedData = sanitized; _cachedData = sanitized;
_forceFullReapply = true;
return; return;
} }
@@ -620,118 +601,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
return data; return data;
} }
private void ResetRestoreState()
{
Volatile.Write(ref _restoreRequested, 0);
}
private void EnsureRestoredStateWhileWaitingForData(string reason, bool skipIfAlreadyRestored = true)
{
if (!IsVisible || _charaHandler is null || _charaHandler.Address == nint.Zero)
{
return;
}
if (_cachedData is not null || LastReceivedCharacterData is not null)
{
return;
}
if (!skipIfAlreadyRestored)
{
ResetRestoreState();
}
else if (Volatile.Read(ref _restoreRequested) == 1)
{
return;
}
if (Interlocked.CompareExchange(ref _restoreRequested, 1, 0) != 0)
{
return;
}
var applicationId = Guid.NewGuid();
_ = Task.Run(async () =>
{
try
{
Logger.LogDebug("[{applicationId}] Restoring vanilla state for {handler} while waiting for data ({reason})", applicationId, GetLogIdentifier(), reason);
await RevertToRestoredAsync(applicationId).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.LogDebug(ex, "[{applicationId}] Failed to restore vanilla state for {handler} ({reason})", applicationId, GetLogIdentifier(), reason);
ResetRestoreState();
}
});
}
private static Dictionary<ObjectKind, HashSet<PlayerChanges>> BuildFullChangeSet(CharacterData characterData)
{
var result = new Dictionary<ObjectKind, HashSet<PlayerChanges>>();
foreach (var objectKind in Enum.GetValues<ObjectKind>())
{
var changes = new HashSet<PlayerChanges>();
if (characterData.FileReplacements.TryGetValue(objectKind, out var replacements) && replacements.Count > 0)
{
changes.Add(PlayerChanges.ModFiles);
if (objectKind == ObjectKind.Player)
{
changes.Add(PlayerChanges.ForcedRedraw);
}
}
if (characterData.GlamourerData.TryGetValue(objectKind, out var glamourer) && !string.IsNullOrEmpty(glamourer))
{
changes.Add(PlayerChanges.Glamourer);
}
if (characterData.CustomizePlusData.TryGetValue(objectKind, out var customize) && !string.IsNullOrEmpty(customize))
{
changes.Add(PlayerChanges.Customize);
}
if (objectKind == ObjectKind.Player)
{
if (!string.IsNullOrEmpty(characterData.ManipulationData))
{
changes.Add(PlayerChanges.ModManip);
changes.Add(PlayerChanges.ForcedRedraw);
}
if (!string.IsNullOrEmpty(characterData.HeelsData))
{
changes.Add(PlayerChanges.Heels);
}
if (!string.IsNullOrEmpty(characterData.HonorificData))
{
changes.Add(PlayerChanges.Honorific);
}
if (!string.IsNullOrEmpty(characterData.MoodlesData))
{
changes.Add(PlayerChanges.Moodles);
}
if (!string.IsNullOrEmpty(characterData.PetNamesData))
{
changes.Add(PlayerChanges.PetNames);
}
}
if (changes.Count > 0)
{
result[objectKind] = changes;
}
}
return result;
}
private bool CanApplyNow() private bool CanApplyNow()
{ {
return !_dalamudUtil.IsInCombat return !_dalamudUtil.IsInCombat
@@ -825,7 +694,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
this, forceApplyCustomization, forceApplyMods: false) this, forceApplyCustomization, forceApplyMods: false)
.Any(p => p.Value.Contains(PlayerChanges.ModManip) || p.Value.Contains(PlayerChanges.ModFiles)); .Any(p => p.Value.Contains(PlayerChanges.ModManip) || p.Value.Contains(PlayerChanges.ModFiles));
_forceApplyMods = hasDiffMods || _forceApplyMods || _cachedData == null; _forceApplyMods = hasDiffMods || _forceApplyMods || _cachedData == null;
_forceFullReapply = true;
_cachedData = characterData; _cachedData = characterData;
Logger.LogDebug("[BASE-{appBase}] Setting data: {hash}, forceApplyMods: {force}", applicationBase, _cachedData.DataHash.Value, _forceApplyMods); Logger.LogDebug("[BASE-{appBase}] Setting data: {hash}, forceApplyMods: {force}", applicationBase, _cachedData.DataHash.Value, _forceApplyMods);
} }
@@ -847,11 +715,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
var charaDataToUpdate = characterData.CheckUpdatedData(applicationBase, _cachedData?.DeepClone() ?? new(), Logger, this, forceApplyCustomization, _forceApplyMods); var charaDataToUpdate = characterData.CheckUpdatedData(applicationBase, _cachedData?.DeepClone() ?? new(), Logger, this, forceApplyCustomization, _forceApplyMods);
if (_forceFullReapply)
{
charaDataToUpdate = BuildFullChangeSet(characterData);
}
if (handlerReady && _forceApplyMods) if (handlerReady && _forceApplyMods)
{ {
_forceApplyMods = false; _forceApplyMods = false;
@@ -870,9 +733,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
Logger.LogDebug("[BASE-{appbase}] Downloading and applying character for {name}", applicationBase, GetPrimaryAliasOrUidSafe()); Logger.LogDebug("[BASE-{appbase}] Downloading and applying character for {name}", applicationBase, GetPrimaryAliasOrUidSafe());
var forcesReapply = _forceFullReapply || forceApplyCustomization || LastAppliedApproximateVRAMBytes < 0 || LastAppliedDataTris < 0; DownloadAndApplyCharacter(applicationBase, characterData.DeepClone(), charaDataToUpdate);
DownloadAndApplyCharacter(applicationBase, characterData.DeepClone(), charaDataToUpdate, forcesReapply, forceApplyCustomization);
} }
public override string ToString() public override string ToString()
@@ -1064,185 +925,113 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
} }
} }
private void DownloadAndApplyCharacter(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData, bool forcePerformanceRecalc, bool forceApplyCustomization) private void DownloadAndApplyCharacter(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData)
{ {
if (!updatedData.Any()) if (!updatedData.Any())
{ {
if (forcePerformanceRecalc) Logger.LogDebug("[BASE-{appBase}] Nothing to update for {obj}", applicationBase, GetLogIdentifier());
{ return;
updatedData = BuildFullChangeSet(charaData);
}
if (!updatedData.Any())
{
Logger.LogDebug("[BASE-{appBase}] Nothing to update for {obj}", applicationBase, GetLogIdentifier());
_forceFullReapply = false;
return;
}
} }
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));
if (_downloadInProgress)
{
Logger.LogDebug("[BASE-{appBase}] Download already in progress for {handler}, queueing data", applicationBase, GetLogIdentifier());
EnqueueDeferredCharacterData(charaData, forceApplyCustomization || _forceApplyMods);
return;
}
_downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate() ?? new CancellationTokenSource(); _downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate() ?? new CancellationTokenSource();
var downloadToken = _downloadCancellationTokenSource.Token; var downloadToken = _downloadCancellationTokenSource.Token;
var downloadOwnerToken = Guid.NewGuid(); _ = DownloadAndApplyCharacterAsync(applicationBase, charaData, updatedData, updateModdedPaths, updateManip, downloadToken).ConfigureAwait(false);
_currentDownloadOwnerToken = downloadOwnerToken;
_downloadInProgress = true;
_ = DownloadAndApplyCharacterAsync(applicationBase, charaData, updatedData, updateModdedPaths, updateManip, forcePerformanceRecalc, downloadOwnerToken, 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, bool forcePerformanceRecalc, Guid downloadOwnerToken, CancellationToken downloadToken) bool updateModdedPaths, bool updateManip, CancellationToken downloadToken)
{ {
try await using var concurrencyLease = await _pairProcessingLimiter.AcquireAsync(downloadToken).ConfigureAwait(false);
Dictionary<(string GamePath, string? Hash), string> moddedPaths = [];
bool skipDownscaleForPair = ShouldSkipDownscale();
var user = GetPrimaryUserData();
if (updateModdedPaths)
{ {
await using var concurrencyLease = await _pairProcessingLimiter.AcquireAsync(downloadToken).ConfigureAwait(false); int attempts = 0;
Dictionary<(string GamePath, string? Hash), string> moddedPaths = []; List<FileReplacementData> toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
bool skipDownscaleForPair = ShouldSkipDownscale();
var user = GetPrimaryUserData();
bool performedDownload = false; while (toDownloadReplacements.Count > 0 && attempts++ <= 10 && !downloadToken.IsCancellationRequested)
if (updateModdedPaths)
{ {
int attempts = 0; if (_pairDownloadTask != null && !_pairDownloadTask.IsCompleted)
List<FileReplacementData> toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
Logger.LogDebug("[BASE-{appBase}] Initial missing files for {handler}: {count}", applicationBase, GetLogIdentifier(), toDownloadReplacements.Count);
while (toDownloadReplacements.Count > 0 && attempts++ <= 10 && !downloadToken.IsCancellationRequested)
{ {
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}] 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); 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, Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Informational,
$"Starting download for {toDownloadReplacements.Count} files"))); $"Starting download for {toDownloadReplacements.Count} files")));
var currentHandler = _charaHandler; var toDownloadFiles = await _downloadManager.InitiateDownloadList(_charaHandler!, toDownloadReplacements, downloadToken).ConfigureAwait(false);
var toDownloadFiles = await _downloadManager.InitiateDownloadList(currentHandler, toDownloadReplacements, downloadToken, downloadOwnerToken).ConfigureAwait(false);
Logger.LogDebug("[BASE-{appBase}] Download plan prepared for {handler}: {current} transfers, forbidden so far: {forbidden}", applicationBase, GetLogIdentifier(), toDownloadFiles.Count, _downloadManager.ForbiddenTransfers.Count);
if (!_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, toDownloadFiles)) if (!_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, toDownloadFiles))
{ {
_downloadManager.ClearDownload(); _downloadManager.ClearDownload();
MarkApplicationDeferred(charaData);
return; return;
} }
performedDownload = true; var handlerForDownload = _charaHandler;
_pairDownloadTask = Task.Run(async () => await _downloadManager.DownloadFiles(handlerForDownload, toDownloadReplacements, downloadToken, skipDownscaleForPair).ConfigureAwait(false));
var handlerForDownload = currentHandler;
_pairDownloadTask = Task.Run(async () => await _downloadManager.DownloadFiles(handlerForDownload, toDownloadReplacements, downloadToken, skipDownscaleForPair).ConfigureAwait(false));
await _pairDownloadTask.ConfigureAwait(false); await _pairDownloadTask.ConfigureAwait(false);
if (downloadToken.IsCancellationRequested) if (downloadToken.IsCancellationRequested)
{ {
Logger.LogTrace("[BASE-{appBase}] Detected cancellation", applicationBase); Logger.LogTrace("[BASE-{appBase}] Detected cancellation", applicationBase);
MarkApplicationDeferred(charaData);
return; return;
} }
toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken); toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
if (toDownloadReplacements.TrueForAll(c => _downloadManager.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, c.Hash, StringComparison.Ordinal)))) 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}] Re-evaluating missing files for {handler}: {count} remaining after attempt {attempt}", applicationBase, GetLogIdentifier(), toDownloadReplacements.Count, attempts);
}
if (!performedDownload)
{
if (!_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, new List<DownloadFileTransfer>()))
{ {
_downloadManager.ClearDownload(); break;
MarkApplicationDeferred(charaData);
return;
} }
await Task.Delay(TimeSpan.FromSeconds(2), downloadToken).ConfigureAwait(false);
} }
if (!await _playerPerformanceService.CheckBothThresholds(this, charaData).ConfigureAwait(false)) if (!await _playerPerformanceService.CheckBothThresholds(this, charaData).ConfigureAwait(false))
{ {
MarkApplicationDeferred(charaData);
return; return;
} }
} }
else if (forcePerformanceRecalc)
downloadToken.ThrowIfCancellationRequested();
var handlerForApply = _charaHandler;
if (handlerForApply is null || handlerForApply.Address == nint.Zero)
{ {
if (!_playerPerformanceService.ComputeAndAutoPauseOnVRAMUsageThresholds(this, charaData, new List<DownloadFileTransfer>())) Logger.LogDebug("[BASE-{appBase}] Handler not available for {player}, cached data for later application", applicationBase, GetLogIdentifier());
{ _cachedData = charaData;
MarkApplicationDeferred(charaData); _pairStateCache.Store(Ident, charaData);
return; return;
}
if (!await _playerPerformanceService.CheckBothThresholds(this, charaData).ConfigureAwait(false))
{
MarkApplicationDeferred(charaData);
return;
}
} }
downloadToken.ThrowIfCancellationRequested(); var appToken = _applicationCancellationTokenSource?.Token;
while ((!_applicationTask?.IsCompleted ?? false)
var handlerForApply = _charaHandler; && !downloadToken.IsCancellationRequested
if (handlerForApply is null || handlerForApply.Address == nint.Zero) && (!appToken?.IsCancellationRequested ?? false))
{
Logger.LogDebug("[BASE-{appBase}] Handler not available for {player}, cached data for later application", applicationBase, GetLogIdentifier());
_cachedData = charaData;
_pairStateCache.Store(Ident, charaData);
_forceFullReapply = true;
MarkApplicationDeferred(charaData);
return;
}
var appToken = _applicationCancellationTokenSource?.Token;
while ((!_applicationTask?.IsCompleted ?? false)
&& !downloadToken.IsCancellationRequested
&& (!appToken?.IsCancellationRequested ?? false))
{
// block until current application is done
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 || (appToken?.IsCancellationRequested ?? false))
{
MarkApplicationDeferred(charaData);
return;
}
_applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource();
var token = _applicationCancellationTokenSource.Token;
_forceFullReapply = false;
_applicationTask = ApplyCharacterDataAsync(applicationBase, handlerForApply, charaData, updatedData, updateModdedPaths, updateManip, moddedPaths, token);
}
catch (OperationCanceledException ex) when (downloadToken.IsCancellationRequested || ex.CancellationToken == downloadToken)
{ {
Logger.LogDebug("[BASE-{appBase}] Download cancelled for {handler}", applicationBase, GetLogIdentifier()); Logger.LogDebug("[BASE-{appBase}] Waiting for current data application (Id: {id}) for player ({handler}) to finish", applicationBase, _applicationId, PlayerName);
MarkApplicationDeferred(charaData); await Task.Delay(250).ConfigureAwait(false);
} }
finally
if (downloadToken.IsCancellationRequested || (appToken?.IsCancellationRequested ?? false))
{ {
_downloadInProgress = false; return;
} }
_applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource();
var token = _applicationCancellationTokenSource.Token;
_applicationTask = ApplyCharacterDataAsync(applicationBase, handlerForApply, charaData, updatedData, updateModdedPaths, updateManip, moddedPaths, token);
} }
private async Task ApplyCharacterDataAsync(Guid applicationBase, GameObjectHandler handlerForApply, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData, bool updateModdedPaths, bool updateManip, private async Task ApplyCharacterDataAsync(Guid applicationBase, GameObjectHandler handlerForApply, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData, bool updateModdedPaths, bool updateManip,
@@ -1265,7 +1054,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
if (penumbraCollection == Guid.Empty) if (penumbraCollection == Guid.Empty)
{ {
Logger.LogTrace("[BASE-{applicationId}] Penumbra collection unavailable for {handler}, caching data for later application", applicationBase, GetLogIdentifier()); Logger.LogTrace("[BASE-{applicationId}] Penumbra collection unavailable for {handler}, caching data for later application", applicationBase, GetLogIdentifier());
MarkApplicationDeferred(charaData); _cachedData = charaData;
_pairStateCache.Store(Ident, charaData);
return; return;
} }
} }
@@ -1282,7 +1072,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
if (!objIndex.HasValue) if (!objIndex.HasValue)
{ {
Logger.LogDebug("[BASE-{applicationId}] GameObject not available for {handler}, caching data for later application", applicationBase, GetLogIdentifier()); Logger.LogDebug("[BASE-{applicationId}] GameObject not available for {handler}, caching data for later application", applicationBase, GetLogIdentifier());
MarkApplicationDeferred(charaData); _cachedData = charaData;
_pairStateCache.Store(Ident, charaData);
return; return;
} }
@@ -1325,22 +1116,20 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
Logger.LogDebug("[{applicationId}] Application finished", _applicationId); Logger.LogDebug("[{applicationId}] Application finished", _applicationId);
} }
catch (OperationCanceledException ex) when (ex.CancellationToken == token || token.IsCancellationRequested) catch (OperationCanceledException)
{ {
Logger.LogDebug("[{applicationId}] Application cancelled via request token for {handler}", _applicationId, GetLogIdentifier()); Logger.LogDebug("[{applicationId}] Application cancelled for {handler}", _applicationId, GetLogIdentifier());
MarkApplicationDeferred(charaData); _cachedData = charaData;
} _pairStateCache.Store(Ident, charaData);
catch (OperationCanceledException ex)
{
MarkApplicationDeferred(charaData);
Logger.LogDebug("[{applicationId}] Application deferred; redraw or apply operation cancelled ({reason}) for {handler}", _applicationId, ex.Message, GetLogIdentifier());
} }
catch (Exception ex) catch (Exception ex)
{ {
if (ex is AggregateException aggr && aggr.InnerExceptions.Any(e => e is ArgumentNullException)) if (ex is AggregateException aggr && aggr.InnerExceptions.Any(e => e is ArgumentNullException))
{ {
IsVisible = false; IsVisible = false;
MarkApplicationDeferred(charaData); _forceApplyMods = true;
_cachedData = charaData;
_pairStateCache.Store(Ident, charaData);
Logger.LogDebug("[{applicationId}] Cancelled, player turned null during application", _applicationId); Logger.LogDebug("[{applicationId}] Cancelled, player turned null during application", _applicationId);
} }
else else
@@ -1403,7 +1192,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
else else
{ {
Logger.LogTrace("{handler} visibility changed, now: {visi}, no cached or received data exists", GetLogIdentifier(), IsVisible); Logger.LogTrace("{handler} visibility changed, now: {visi}, no cached or received data exists", GetLogIdentifier(), IsVisible);
EnsureRestoredStateWhileWaitingForData("VisibleWithoutCachedData");
} }
} }
else if (_charaHandler?.Address == nint.Zero && IsVisible) else if (_charaHandler?.Address == nint.Zero && IsVisible)
@@ -1735,27 +1523,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
pending.CharacterData, pending.Forced); pending.CharacterData, pending.Forced);
} }
private void MarkApplicationDeferred(CharacterData charaData)
{
_forceApplyMods = true;
_forceFullReapply = true;
_currentDownloadOwnerToken = Guid.Empty;
_cachedData = charaData;
_pairStateCache.Store(Ident, charaData);
EnqueueDeferredCharacterData(charaData);
}
private void EnqueueDeferredCharacterData(CharacterData charaData, bool forced = true)
{
try
{
_dataReceivedInDowntime = new(Guid.NewGuid(), charaData.DeepClone(), forced);
}
catch (Exception ex)
{
Logger.LogDebug(ex, "Failed to queue deferred data for {handler}", GetLogIdentifier());
}
}
} }
internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory

View File

@@ -1,17 +1,16 @@
using System; using System;
using LightlessSync.API.Data;
using LightlessSync.API.Data.Comparer;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Utils;
using LightlessSync.Services.Mediator;
using LightlessSync.Services;
using LightlessSync.WebAPI;
using LightlessSync.WebAPI.Files;
using Microsoft.Extensions.Logging;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using LightlessSync.API.Data;
using LightlessSync.API.Data.Comparer;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Utils;
using LightlessSync.WebAPI;
using LightlessSync.WebAPI.Files;
using Microsoft.Extensions.Logging;
namespace LightlessSync.PlayerData.Pairs; namespace LightlessSync.PlayerData.Pairs;
@@ -28,9 +27,6 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
private readonly HashSet<UserData> _usersToPushDataTo = new(UserDataComparer.Instance); private readonly HashSet<UserData> _usersToPushDataTo = new(UserDataComparer.Instance);
private readonly SemaphoreSlim _pushDataSemaphore = new(1, 1); private readonly SemaphoreSlim _pushDataSemaphore = new(1, 1);
private readonly CancellationTokenSource _runtimeCts = new(); private readonly CancellationTokenSource _runtimeCts = new();
private readonly Dictionary<string, string> _lastPushedHashes = new(StringComparer.Ordinal);
private readonly object _pushSync = new();
public VisibleUserDataDistributor(ILogger<VisibleUserDataDistributor> logger, ApiController apiController, DalamudUtilService dalamudUtil, public VisibleUserDataDistributor(ILogger<VisibleUserDataDistributor> logger, ApiController apiController, DalamudUtilService dalamudUtil,
PairLedger pairLedger, LightlessMediator mediator, FileUploadManager fileTransferManager) : base(logger, mediator) PairLedger pairLedger, LightlessMediator mediator, FileUploadManager fileTransferManager) : base(logger, mediator)
@@ -56,7 +52,14 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
}); });
Mediator.Subscribe<ConnectedMessage>(this, (_) => PushToAllVisibleUsers()); Mediator.Subscribe<ConnectedMessage>(this, (_) => PushToAllVisibleUsers());
Mediator.Subscribe<DisconnectedMessage>(this, (_) => HandleDisconnected()); Mediator.Subscribe<DisconnectedMessage>(this, (_) =>
{
_fileTransferManager.CancelUpload();
_previouslyVisiblePlayers.Clear();
_usersToPushDataTo.Clear();
_uploadingCharacterData = null;
_fileUploadTask = null;
});
} }
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
@@ -72,18 +75,15 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
private void PushToAllVisibleUsers(bool forced = false) private void PushToAllVisibleUsers(bool forced = false)
{ {
lock (_pushSync) foreach (var user in GetVisibleUsers())
{ {
foreach (var user in GetVisibleUsers()) _usersToPushDataTo.Add(user);
{ }
_usersToPushDataTo.Add(user);
}
if (_usersToPushDataTo.Count > 0) if (_usersToPushDataTo.Count > 0)
{ {
Logger.LogDebug("Pushing data {hash} for {count} visible players", _lastCreatedData?.DataHash.Value ?? "UNKNOWN", _usersToPushDataTo.Count); Logger.LogDebug("Pushing data {hash} for {count} visible players", _lastCreatedData?.DataHash.Value ?? "UNKNOWN", _usersToPushDataTo.Count);
PushCharacterData_internalLocked(forced); PushCharacterData(forced);
}
} }
} }
@@ -92,9 +92,7 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
if (!_dalamudUtil.GetIsPlayerPresent() || !_apiController.IsConnected) return; if (!_dalamudUtil.GetIsPlayerPresent() || !_apiController.IsConnected) return;
var allVisibleUsers = GetVisibleUsers(); var allVisibleUsers = GetVisibleUsers();
var newVisibleUsers = allVisibleUsers var newVisibleUsers = allVisibleUsers.Except(_previouslyVisiblePlayers, UserDataComparer.Instance).ToList();
.Except(_previouslyVisiblePlayers, UserDataComparer.Instance)
.ToList();
_previouslyVisiblePlayers.Clear(); _previouslyVisiblePlayers.Clear();
_previouslyVisiblePlayers.AddRange(allVisibleUsers); _previouslyVisiblePlayers.AddRange(allVisibleUsers);
if (newVisibleUsers.Count == 0) return; if (newVisibleUsers.Count == 0) return;
@@ -102,115 +100,48 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
Logger.LogDebug("Scheduling character data push of {data} to {users}", Logger.LogDebug("Scheduling character data push of {data} to {users}",
_lastCreatedData?.DataHash.Value ?? string.Empty, _lastCreatedData?.DataHash.Value ?? string.Empty,
string.Join(", ", newVisibleUsers.Select(k => k.AliasOrUID))); string.Join(", ", newVisibleUsers.Select(k => k.AliasOrUID)));
lock (_pushSync) foreach (var user in newVisibleUsers)
{ {
foreach (var user in newVisibleUsers) _usersToPushDataTo.Add(user);
{
_usersToPushDataTo.Add(user);
}
PushCharacterData_internalLocked();
} }
PushCharacterData();
} }
private void PushCharacterData(bool forced = false) private void PushCharacterData(bool forced = false)
{
lock (_pushSync)
{
PushCharacterData_internalLocked(forced);
}
}
private void PushCharacterData_internalLocked(bool forced = false)
{ {
if (_lastCreatedData == null || _usersToPushDataTo.Count == 0) return; if (_lastCreatedData == null || _usersToPushDataTo.Count == 0) return;
if (!_apiController.IsConnected || !_fileTransferManager.IsReady)
{
Logger.LogTrace("Skipping character push. Connected: {connected}, UploadManagerReady: {ready}",
_apiController.IsConnected, _fileTransferManager.IsReady);
return;
}
_ = Task.Run(async () => _ = Task.Run(async () =>
{ {
try try
{ {
Task<CharacterData>? uploadTask; forced |= _uploadingCharacterData?.DataHash != _lastCreatedData.DataHash;
bool forcedPush;
lock (_pushSync)
{
if (_lastCreatedData == null || _usersToPushDataTo.Count == 0) return;
forcedPush = forced | (_uploadingCharacterData?.DataHash != _lastCreatedData.DataHash);
if (_fileUploadTask == null || (_fileUploadTask?.IsCompleted ?? false) || forcedPush) if (_fileUploadTask == null || (_fileUploadTask?.IsCompleted ?? false) || forced)
{ {
_uploadingCharacterData = _lastCreatedData.DeepClone(); _uploadingCharacterData = _lastCreatedData.DeepClone();
Logger.LogDebug("Starting UploadTask for {hash}, Reason: TaskIsNull: {task}, TaskIsCompleted: {taskCpl}, Forced: {frc}", Logger.LogDebug("Starting UploadTask for {hash}, Reason: TaskIsNull: {task}, TaskIsCompleted: {taskCpl}, Forced: {frc}",
_lastCreatedData.DataHash, _fileUploadTask == null, _fileUploadTask?.IsCompleted ?? false, forcedPush); _lastCreatedData.DataHash, _fileUploadTask == null, _fileUploadTask?.IsCompleted ?? false, forced);
_fileUploadTask = _fileTransferManager.UploadFiles(_uploadingCharacterData, [.. _usersToPushDataTo]); _fileUploadTask = _fileTransferManager.UploadFiles(_uploadingCharacterData, [.. _usersToPushDataTo]);
} }
uploadTask = _fileUploadTask; if (_fileUploadTask != null)
} {
var dataToSend = await _fileUploadTask.ConfigureAwait(false);
var dataToSend = await uploadTask.ConfigureAwait(false);
var dataHash = dataToSend.DataHash.Value;
await _pushDataSemaphore.WaitAsync(_runtimeCts.Token).ConfigureAwait(false); await _pushDataSemaphore.WaitAsync(_runtimeCts.Token).ConfigureAwait(false);
try try
{ {
List<UserData> recipients; if (_usersToPushDataTo.Count == 0) return;
bool shouldSkip = false; Logger.LogDebug("Pushing {data} to {users}", dataToSend.DataHash, string.Join(", ", _usersToPushDataTo.Select(k => k.AliasOrUID)));
lock (_pushSync) await _apiController.PushCharacterData(dataToSend, [.. _usersToPushDataTo]).ConfigureAwait(false);
{ _usersToPushDataTo.Clear();
if (_usersToPushDataTo.Count == 0) return;
recipients = forcedPush
? _usersToPushDataTo.ToList()
: _usersToPushDataTo
.Where(user => !_lastPushedHashes.TryGetValue(user.UID, out var sentHash) || !string.Equals(sentHash, dataHash, StringComparison.Ordinal))
.ToList();
if (recipients.Count == 0 && !forcedPush)
{
Logger.LogTrace("All recipients already have character data hash {hash}, skipping push.", dataHash);
_usersToPushDataTo.Clear();
shouldSkip = true;
}
}
if (shouldSkip)
return;
Logger.LogDebug("Pushing {data} to {users}", dataHash, string.Join(", ", recipients.Select(k => k.AliasOrUID)));
await _apiController.PushCharacterData(dataToSend, recipients).ConfigureAwait(false);
lock (_pushSync)
{
foreach (var user in recipients)
{
_lastPushedHashes[user.UID] = dataHash;
_usersToPushDataTo.Remove(user);
}
if (!forcedPush && _usersToPushDataTo.Count > 0)
{
foreach (var satisfied in _usersToPushDataTo
.Where(user => _lastPushedHashes.TryGetValue(user.UID, out var sentHash) && string.Equals(sentHash, dataHash, StringComparison.Ordinal))
.ToList())
{
_usersToPushDataTo.Remove(satisfied);
}
}
if (forcedPush)
{
_usersToPushDataTo.Clear();
}
}
} }
finally finally
{ {
_pushDataSemaphore.Release(); _pushDataSemaphore.Release();
} }
} }
}
catch (OperationCanceledException) when (_runtimeCts.IsCancellationRequested) catch (OperationCanceledException) when (_runtimeCts.IsCancellationRequested)
{ {
Logger.LogDebug("PushCharacterData cancelled"); Logger.LogDebug("PushCharacterData cancelled");
@@ -222,20 +153,6 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
}); });
} }
private void HandleDisconnected()
{
_fileTransferManager.CancelUpload();
_previouslyVisiblePlayers.Clear();
lock (_pushSync)
{
_usersToPushDataTo.Clear();
_lastPushedHashes.Clear();
_uploadingCharacterData = null;
_fileUploadTask = null;
}
}
private List<UserData> GetVisibleUsers() private List<UserData> GetVisibleUsers()
{ {
return _pairLedger.GetVisiblePairs() return _pairLedger.GetVisiblePairs()