diff --git a/MareSynchronos/PlayerData/Pairs/CachedPlayer.cs b/MareSynchronos/PlayerData/Pairs/CachedPlayer.cs index 7e3a4c3..9cd319b 100644 --- a/MareSynchronos/PlayerData/Pairs/CachedPlayer.cs +++ b/MareSynchronos/PlayerData/Pairs/CachedPlayer.cs @@ -24,7 +24,7 @@ public sealed class CachedPlayer : DisposableMediatorSubscriberBase private readonly Func, bool, GameObjectHandler> _gameObjectHandlerFactory; private readonly IpcManager _ipcManager; private readonly IHostApplicationLifetime _lifetime; - private CancellationTokenSource _applicationCancellationTokenSource = new(); + private CancellationTokenSource? _applicationCancellationTokenSource = new(); private Guid _applicationId; private Task? _applicationTask; private CharacterData _cachedData = new(); @@ -158,37 +158,35 @@ public sealed class CachedPlayer : DisposableMediatorSubscriberBase try { Guid applicationId = Guid.NewGuid(); - _applicationCancellationTokenSource.Cancel(); - _applicationCancellationTokenSource.Dispose(); - _downloadCancellationTokenSource?.Cancel(); - _downloadCancellationTokenSource?.Dispose(); + _applicationCancellationTokenSource?.CancelDispose(); + _applicationCancellationTokenSource = null; + _downloadCancellationTokenSource?.CancelDispose(); _downloadCancellationTokenSource = null; _charaHandler?.Dispose(); _charaHandler = null; - if (!_lifetime.ApplicationStopping.IsCancellationRequested) - { - if (_dalamudUtil.IsZoning) - { - Logger.LogTrace("[{applicationId}] Removing temp collection for {name} ({OnlineUser})", applicationId, name, OnlineUser); - _ipcManager.PenumbraRemoveTemporaryCollection(Logger, applicationId, name); - } - else if (!_dalamudUtil.IsZoning && !_dalamudUtil.IsInCutscene) - { - Logger.LogTrace("[{applicationId}] Restoring state for {name} ({OnlineUser})", applicationId, name, OnlineUser); - _ipcManager.PenumbraRemoveTemporaryCollection(Logger, applicationId, name); + if (_lifetime.ApplicationStopping.IsCancellationRequested) return; - foreach (KeyValuePair> item in _cachedData.FileReplacements) + if (_dalamudUtil.IsZoning) + { + Logger.LogTrace("[{applicationId}] Removing temp collection for {name} ({OnlineUser})", applicationId, name, OnlineUser); + _ipcManager.PenumbraRemoveTemporaryCollection(Logger, applicationId, name); + } + else if (_dalamudUtil is { IsZoning: false, IsInCutscene: false }) + { + Logger.LogTrace("[{applicationId}] Restoring state for {name} ({OnlineUser})", applicationId, name, OnlineUser); + _ipcManager.PenumbraRemoveTemporaryCollection(Logger, applicationId, name); + + foreach (KeyValuePair> item in _cachedData.FileReplacements) + { + try { - try - { - RevertCustomizationData(item.Key, name, applicationId).GetAwaiter().GetResult(); - } - catch (InvalidOperationException ex) - { - Logger.LogWarning(ex, "Failed disposing player (not present anymore?)"); - break; - } + RevertCustomizationData(item.Key, name, applicationId).GetAwaiter().GetResult(); + } + catch (InvalidOperationException ex) + { + Logger.LogWarning(ex, "Failed disposing player (not present anymore?)"); + break; } } } @@ -381,9 +379,7 @@ public sealed class CachedPlayer : DisposableMediatorSubscriberBase var updateModdedPaths = updatedData.Values.Any(v => v.Any(p => p == PlayerChanges.Mods)); - _downloadCancellationTokenSource?.Cancel(); - _downloadCancellationTokenSource?.Dispose(); - _downloadCancellationTokenSource = new CancellationTokenSource(); + _downloadCancellationTokenSource = _downloadCancellationTokenSource?.CancelRecreate(); var downloadToken = _downloadCancellationTokenSource.Token; Task.Run(async () => @@ -421,17 +417,19 @@ public sealed class CachedPlayer : DisposableMediatorSubscriberBase } } - while ((!_applicationTask?.IsCompleted ?? false) && !downloadToken.IsCancellationRequested && !_applicationCancellationTokenSource.IsCancellationRequested) + var appToken = _applicationCancellationTokenSource?.Token; + while ((!_applicationTask?.IsCompleted ?? false) + && !downloadToken.IsCancellationRequested + && (!appToken?.IsCancellationRequested ?? false)) { // block until current application is done Logger.LogDebug("Waiting for current data application (Id: {id}) for player ({handler}) to finish", _applicationId, PlayerName); await Task.Delay(250).ConfigureAwait(false); } - if (downloadToken.IsCancellationRequested || _applicationCancellationTokenSource.IsCancellationRequested) return; + if (downloadToken.IsCancellationRequested || (appToken?.IsCancellationRequested ?? false)) return; - _applicationCancellationTokenSource?.Dispose(); - _applicationCancellationTokenSource = new(); + _applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate(); var token = _applicationCancellationTokenSource.Token; _applicationTask = Task.Run(async () => { @@ -441,7 +439,7 @@ public sealed class CachedPlayer : DisposableMediatorSubscriberBase Logger.LogDebug("[{applicationId}] Starting application task", _applicationId); Logger.LogDebug("[{applicationId}] Waiting for initial draw for for {handler}", _applicationId, _charaHandler); - await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, _charaHandler, _applicationId, 30000, token).ConfigureAwait(false); + await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, _charaHandler!, _applicationId, 30000, token).ConfigureAwait(false); token.ThrowIfCancellationRequested(); @@ -471,9 +469,7 @@ public sealed class CachedPlayer : DisposableMediatorSubscriberBase { var player = _dalamudUtil.GetCharacterFromObjectTableByIndex(msg.ObjTblIdx); if (player == null || !string.Equals(player.Name.ToString(), PlayerName, StringComparison.OrdinalIgnoreCase)) return; - _redrawCts.Cancel(); - _redrawCts.Dispose(); - _redrawCts = new(); + _redrawCts = _redrawCts.CancelRecreate(); _redrawCts.CancelAfter(TimeSpan.FromSeconds(30)); var token = _redrawCts.Token; @@ -484,7 +480,7 @@ public sealed class CachedPlayer : DisposableMediatorSubscriberBase Logger.LogDebug("Unauthorized character change detected"); await ApplyCustomizationData(applicationId, new(ObjectKind.Player, new HashSet(new[] { PlayerChanges.Palette, PlayerChanges.Customize, PlayerChanges.Heels, PlayerChanges.Mods })), - _cachedData, _applicationCancellationTokenSource.Token).ConfigureAwait(false); + _cachedData, token).ConfigureAwait(false); }, token); } diff --git a/MareSynchronos/PlayerData/Pairs/OnlinePlayerManager.cs b/MareSynchronos/PlayerData/Pairs/OnlinePlayerManager.cs index 0cc9430..ff6108c 100644 --- a/MareSynchronos/PlayerData/Pairs/OnlinePlayerManager.cs +++ b/MareSynchronos/PlayerData/Pairs/OnlinePlayerManager.cs @@ -54,7 +54,7 @@ public class OnlinePlayerManager : DisposableMediatorSubscriberBase if (pair.InitializePair(pChar.Name.ToString())) { - newVisiblePlayers.Add(pair.UserData ?? pair.GroupPair.First().Value.User); + newVisiblePlayers.Add(pair.UserData); } } diff --git a/MareSynchronos/PlayerData/Pairs/Pair.cs b/MareSynchronos/PlayerData/Pairs/Pair.cs index 3ee1371..abce7e2 100644 --- a/MareSynchronos/PlayerData/Pairs/Pair.cs +++ b/MareSynchronos/PlayerData/Pairs/Pair.cs @@ -35,7 +35,7 @@ public class Pair public bool CachedPlayerExists => CachedPlayer?.CheckExistence() ?? false; public Dictionary GroupPair { get; set; } = new(GroupDtoComparer.Instance); - public bool HasCachedPlayer => CachedPlayer != null && !string.IsNullOrEmpty(CachedPlayer.PlayerName); + public bool HasCachedPlayer => CachedPlayer != null && !string.IsNullOrEmpty(CachedPlayer.PlayerName) && _onlineUserIdentDto != null; public bool IsOnline => CachedPlayer != null; public bool IsPaused => UserPair != null && UserPair.OtherPermissions.IsPaired() ? UserPair.OtherPermissions.IsPaused() || UserPair.OwnPermissions.IsPaused() @@ -44,7 +44,21 @@ public class Pair public bool IsVisible => CachedPlayer?.PlayerName != null; public CharacterData? LastReceivedCharacterData { get; set; } public string? PlayerName => CachedPlayer?.PlayerName ?? string.Empty; - public string PlayerNameHash => CachedPlayer?.PlayerNameHash ?? string.Empty; + + public string GetPlayerNameHash() + { + try + { + return CachedPlayer?.PlayerNameHash ?? string.Empty; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error accessing PlayerNameHash, recreating CachedPlayer"); + RecreateCachedPlayer(); + } + + return string.Empty; + } public UserData UserData => UserPair?.User ?? GroupPair.First().Value.User; public UserPairDto? UserPair { get; set; } private CachedPlayer? CachedPlayer { get; set; } @@ -138,7 +152,12 @@ public class Pair public void RecreateCachedPlayer(OnlineUserIdentDto? dto = null) { - if (dto == null && _onlineUserIdentDto == null) return; + if (dto == null && _onlineUserIdentDto == null) + { + CachedPlayer?.Dispose(); + CachedPlayer = null; + return; + } if (dto != null) { _onlineUserIdentDto = dto; diff --git a/MareSynchronos/PlayerData/Pairs/PairManager.cs b/MareSynchronos/PlayerData/Pairs/PairManager.cs index 9c14e2b..4cc3f8a 100644 --- a/MareSynchronos/PlayerData/Pairs/PairManager.cs +++ b/MareSynchronos/PlayerData/Pairs/PairManager.cs @@ -94,10 +94,10 @@ public sealed class PairManager : DisposableMediatorSubscriberBase { if (pChar == null) return null; var hash = pChar.GetHash256(); - return GetOnlineUserPairs().Find(p => string.Equals(p.PlayerNameHash, hash, StringComparison.Ordinal)); + return _allClientPairs.Values.FirstOrDefault(f => string.Equals(hash, f.GetPlayerNameHash())); } - public List GetOnlineUserPairs() => _allClientPairs.Where(p => !string.IsNullOrEmpty(p.Value.PlayerNameHash)).Select(p => p.Value).ToList(); + public List GetOnlineUserPairs() => _allClientPairs.Where(p => !string.IsNullOrEmpty(p.Value.GetPlayerNameHash())).Select(p => p.Value).ToList(); public List GetVisibleUsers() => _allClientPairs.Where(p => p.Value.HasCachedPlayer).Select(p => p.Key).ToList(); diff --git a/MareSynchronos/Utils/VariousExtensions.cs b/MareSynchronos/Utils/VariousExtensions.cs index 35d2362..3f49c39 100644 --- a/MareSynchronos/Utils/VariousExtensions.cs +++ b/MareSynchronos/Utils/VariousExtensions.cs @@ -19,4 +19,23 @@ public static class VariousExtensions return ((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)gameObject.Address)->ObjectIndex; } + + public static void CancelDispose(this CancellationTokenSource? cts) + { + try + { + cts?.Cancel(); + cts?.Dispose(); + } + catch(ObjectDisposedException) + { + // swallow it + } + } + + public static CancellationTokenSource CancelRecreate(this CancellationTokenSource? cts) + { + cts.CancelDispose(); + return new CancellationTokenSource(); + } }