From 920090c3b14acf948b25a7217c2bf45732a438ae Mon Sep 17 00:00:00 2001 From: Stanley Dimant Date: Wed, 7 Sep 2022 22:03:17 +0200 Subject: [PATCH] add delayedframework update to dalamudutil, rework to concurrent collections --- .../Factories/CharacterDataFactory.cs | 2 +- MareSynchronos/Managers/CachedPlayer.cs | 54 ++++++++-------- MareSynchronos/Managers/IpcManager.cs | 53 +++++++++------- .../Managers/OnlinePlayerManager.cs | 62 ++++++++++++------- MareSynchronos/Managers/PlayerManager.cs | 11 +--- MareSynchronos/Utils/DalamudUtil.cs | 31 +++++++++- 6 files changed, 128 insertions(+), 85 deletions(-) diff --git a/MareSynchronos/Factories/CharacterDataFactory.cs b/MareSynchronos/Factories/CharacterDataFactory.cs index d9a8606..fd3de0a 100644 --- a/MareSynchronos/Factories/CharacterDataFactory.cs +++ b/MareSynchronos/Factories/CharacterDataFactory.cs @@ -226,7 +226,7 @@ public class CharacterDataFactory previousData.ManipulationString = _ipcManager.PenumbraGetMetaManipulations(); - previousData.GlamourerString[objectKind] = _ipcManager.GlamourerGetCharacterCustomization(chara); + previousData.GlamourerString[objectKind] = _ipcManager.GlamourerGetCharacterCustomization(charaPointer); var human = (Human*)((Character*)charaPointer)->GameObject.GetDrawObject(); for (var mdlIdx = 0; mdlIdx < human->CharacterBase.SlotCount; ++mdlIdx) diff --git a/MareSynchronos/Managers/CachedPlayer.cs b/MareSynchronos/Managers/CachedPlayer.cs index 7df41d9..2380168 100644 --- a/MareSynchronos/Managers/CachedPlayer.cs +++ b/MareSynchronos/Managers/CachedPlayer.cs @@ -47,7 +47,7 @@ public class CachedPlayer private string _originalGlamourerData = string.Empty; - public Dalamud.Game.ClientState.Objects.Types.Character? PlayerCharacter { get; set; } + public IntPtr PlayerCharacter { get; set; } = IntPtr.Zero; public string? PlayerName { get; private set; } @@ -226,27 +226,27 @@ public class CachedPlayer private unsafe void ApplyCustomizationData(ObjectKind objectKind) { - if (PlayerCharacter is null) return; + if (PlayerCharacter == IntPtr.Zero) return; _cachedData.GlamourerData.TryGetValue(objectKind, out var glamourerData); if (objectKind == ObjectKind.Player) { - _dalamudUtil.WaitWhileCharacterIsDrawing(PlayerCharacter.Address); + _dalamudUtil.WaitWhileCharacterIsDrawing(PlayerCharacter); RequestedPenumbraRedraw = true; Logger.Debug( $"Request Redraw for {PlayerName}"); if (_ipcManager.CheckGlamourerApi() && !string.IsNullOrEmpty(glamourerData)) { - _ipcManager.GlamourerApplyAll(glamourerData, PlayerCharacter.Address); + _ipcManager.GlamourerApplyAll(glamourerData, PlayerCharacter); } else { - _ipcManager.PenumbraRedraw(PlayerCharacter.Address); + _ipcManager.PenumbraRedraw(PlayerCharacter); } } else if (objectKind == ObjectKind.MinionOrMount) { - var minionOrMount = ((Character*)PlayerCharacter.Address)->CompanionObject; + var minionOrMount = ((Character*)PlayerCharacter)->CompanionObject; if (minionOrMount != null) { Logger.Debug($"Request Redraw for Minion/Mount"); @@ -262,7 +262,7 @@ public class CachedPlayer } else if (objectKind == ObjectKind.Pet) { - var pet = _dalamudUtil.GetPet(PlayerCharacter.Address); + var pet = _dalamudUtil.GetPet(PlayerCharacter); if (pet != IntPtr.Zero) { Logger.Debug("Request Redraw for Pet"); @@ -278,7 +278,7 @@ public class CachedPlayer } else if (objectKind == ObjectKind.Companion) { - var companion = _dalamudUtil.GetCompanion(PlayerCharacter.Address); + var companion = _dalamudUtil.GetCompanion(PlayerCharacter); if (companion != IntPtr.Zero) { Logger.Debug("Request Redraw for Companion"); @@ -296,7 +296,7 @@ public class CachedPlayer private unsafe void RevertCustomizationData(ObjectKind objectKind) { - if (PlayerCharacter is null) return; + if (PlayerCharacter == IntPtr.Zero) return; if (objectKind == ObjectKind.Player) { @@ -307,12 +307,12 @@ public class CachedPlayer } else { - _ipcManager.PenumbraRedraw(PlayerCharacter.Address); + _ipcManager.PenumbraRedraw(PlayerCharacter); } } else if (objectKind == ObjectKind.MinionOrMount) { - var minionOrMount = ((Character*)PlayerCharacter.Address)->CompanionObject; + var minionOrMount = ((Character*)PlayerCharacter)->CompanionObject; if (minionOrMount != null) { _ipcManager.PenumbraRedraw((IntPtr)minionOrMount); @@ -320,7 +320,7 @@ public class CachedPlayer } else if (objectKind == ObjectKind.Pet) { - var pet = _dalamudUtil.GetPet(PlayerCharacter.Address); + var pet = _dalamudUtil.GetPet(PlayerCharacter); if (pet != IntPtr.Zero) { _ipcManager.PenumbraRedraw(pet); @@ -328,7 +328,7 @@ public class CachedPlayer } else if (objectKind == ObjectKind.Companion) { - var companion = _dalamudUtil.GetCompanion(PlayerCharacter.Address); + var companion = _dalamudUtil.GetCompanion(PlayerCharacter); if (companion != IntPtr.Zero) { _ipcManager.PenumbraRedraw(companion); @@ -345,10 +345,10 @@ public class CachedPlayer try { Logger.Verbose("Restoring state for " + PlayerName); - _dalamudUtil.FrameworkUpdate -= DalamudUtilOnFrameworkUpdate; + _dalamudUtil.DelayedFrameworkUpdate -= DalamudUtilOnDelayedFrameworkUpdate; _ipcManager.PenumbraRedrawEvent -= IpcManagerOnPenumbraRedrawEvent; _ipcManager.PenumbraRemoveTemporaryCollection(PlayerName); - if (PlayerCharacter != null && PlayerCharacter.IsValid()) + if (PlayerCharacter != IntPtr.Zero) { foreach (var item in _cachedData.FileReplacements) { @@ -367,18 +367,18 @@ public class CachedPlayer { _cachedData = new(); PlayerName = string.Empty; - PlayerCharacter = null; + PlayerCharacter = IntPtr.Zero; IsVisible = false; } } - public void InitializePlayer(PlayerCharacter character, CharacterCacheDto? cache) + public void InitializePlayer(IntPtr character, string name, CharacterCacheDto? cache) { Logger.Debug("Initializing Player " + this + " has cache: " + (cache != null)); IsVisible = true; - PlayerName = character.Name.ToString(); + PlayerName = name; PlayerCharacter = character; - _dalamudUtil.FrameworkUpdate += DalamudUtilOnFrameworkUpdate; + _dalamudUtil.DelayedFrameworkUpdate += DalamudUtilOnDelayedFrameworkUpdate; _ipcManager.PenumbraRedrawEvent += IpcManagerOnPenumbraRedrawEvent; _originalGlamourerData = _ipcManager.GlamourerGetCharacterCustomization(PlayerCharacter); _currentCharacterEquipment = new PlayerRelatedObject(ObjectKind.Player, IntPtr.Zero, IntPtr.Zero, @@ -390,12 +390,12 @@ public class CachedPlayer } } - private void DalamudUtilOnFrameworkUpdate() + private void DalamudUtilOnDelayedFrameworkUpdate() { if (!_dalamudUtil.IsPlayerPresent || !_ipcManager.Initialized || !_apiController.IsConnected) return; - PlayerCharacter = _dalamudUtil.GetPlayerCharacterFromObjectTableByName(PlayerName!); - if (PlayerCharacter == null) + PlayerCharacter = _dalamudUtil.GetPlayerCharacterFromObjectTableByName(PlayerName!)?.Address ?? IntPtr.Zero; + if (PlayerCharacter == IntPtr.Zero) { DisposePlayer(); return; @@ -412,7 +412,7 @@ public class CachedPlayer public override string ToString() { - return PlayerNameHash + ":" + PlayerName + ":HasChar " + (PlayerCharacter != null); + return PlayerNameHash + ":" + PlayerName + ":HasChar " + (PlayerCharacter != IntPtr.Zero); } private Task? _penumbraRedrawEventTask; @@ -425,10 +425,10 @@ public class CachedPlayer _penumbraRedrawEventTask = Task.Run(() => { - PlayerCharacter = player; + PlayerCharacter = address; using var cts = new CancellationTokenSource(); cts.CancelAfter(TimeSpan.FromSeconds(5)); - _dalamudUtil.WaitWhileCharacterIsDrawing(PlayerCharacter.Address, cts.Token); + _dalamudUtil.WaitWhileCharacterIsDrawing(PlayerCharacter, cts.Token); if (RequestedPenumbraRedraw == false) { @@ -448,10 +448,10 @@ public class CachedPlayer { Logger.Debug($"Player {PlayerName} changed, PenumbraRedraw is {RequestedPenumbraRedraw}"); _currentCharacterEquipment!.HasUnprocessedUpdate = false; - if (!RequestedPenumbraRedraw && PlayerCharacter is not null) + if (!RequestedPenumbraRedraw && PlayerCharacter != IntPtr.Zero) { Logger.Debug($"Saving new Glamourer data"); - _lastGlamourerData = _ipcManager.GlamourerGetCharacterCustomization(PlayerCharacter!); + _lastGlamourerData = _ipcManager.GlamourerGetCharacterCustomization(PlayerCharacter); } } } \ No newline at end of file diff --git a/MareSynchronos/Managers/IpcManager.cs b/MareSynchronos/Managers/IpcManager.cs index 4731b5f..c545764 100644 --- a/MareSynchronos/Managers/IpcManager.cs +++ b/MareSynchronos/Managers/IpcManager.cs @@ -6,6 +6,7 @@ using Dalamud.Game.ClientState.Objects.Types; using MareSynchronos.Utils; using MareSynchronos.WebAPI; using Action = System.Action; +using System.Collections.Concurrent; namespace MareSynchronos.Managers { @@ -33,7 +34,7 @@ namespace MareSynchronos.Managers private readonly ICallGateSubscriber, string, int, int> _penumbraSetTemporaryMod; private readonly DalamudUtil _dalamudUtil; - private readonly Queue actionQueue = new(); + private readonly ConcurrentQueue actionQueue = new(); public IpcManager(DalamudPluginInterface pi, DalamudUtil dalamudUtil) { @@ -83,8 +84,9 @@ namespace MareSynchronos.Managers private void HandleActionQueue() { - while (actionQueue.TryDequeue(out var action)) + if (actionQueue.TryDequeue(out var action)) { + if (action == null) return; Logger.Debug("Execution action in queue: " + action.Method); action(); } @@ -145,37 +147,50 @@ namespace MareSynchronos.Managers }); } - public void GlamourerApplyOnlyEquipment(string customization, GameObject character) + public void GlamourerApplyOnlyEquipment(string customization, IntPtr character) { if (!CheckGlamourerApi() || string.IsNullOrEmpty(customization)) return; actionQueue.Enqueue(() => { - Logger.Verbose("Glamourer apply only equipment to " + character); - _glamourerApplyOnlyEquipment!.InvokeAction(customization, character); + var gameObj = _dalamudUtil.CreateGameObject(character); + if (gameObj != null) + { + Logger.Verbose("Glamourer apply only equipment to " + character.ToString("X")); + _glamourerApplyOnlyEquipment!.InvokeAction(customization, gameObj); + } }); } - public void GlamourerApplyOnlyCustomization(string customization, GameObject character) + public void GlamourerApplyOnlyCustomization(string customization, IntPtr character) { if (!CheckGlamourerApi() || string.IsNullOrEmpty(customization)) return; actionQueue.Enqueue(() => { - Logger.Verbose("Glamourer apply only customization to " + character); - _glamourerApplyOnlyCustomization!.InvokeAction(customization, character); + var gameObj = _dalamudUtil.CreateGameObject(character); + if (gameObj != null) + { + Logger.Verbose("Glamourer apply only customization to " + character.ToString("X")); + _glamourerApplyOnlyCustomization!.InvokeAction(customization, gameObj); + } }); } - public string GlamourerGetCharacterCustomization(GameObject character) + public string GlamourerGetCharacterCustomization(IntPtr character) { if (!CheckGlamourerApi()) return string.Empty; try { - var glamourerString = _glamourerGetAllCustomization!.InvokeFunc(character); - byte[] bytes = Convert.FromBase64String(glamourerString); - // ignore transparency - bytes[88] = 128; - bytes[89] = 63; - return Convert.ToBase64String(bytes); + var gameObj = _dalamudUtil.CreateGameObject(character); + if (gameObj != null) + { + var glamourerString = _glamourerGetAllCustomization!.InvokeFunc(gameObj); + byte[] bytes = Convert.FromBase64String(glamourerString); + // ignore transparency + bytes[88] = 128; + bytes[89] = 63; + return Convert.ToBase64String(bytes); + } + return string.Empty; } catch { @@ -189,14 +204,6 @@ namespace MareSynchronos.Managers actionQueue.Enqueue(() => _glamourerRevertCustomization!.InvokeAction(character)); } - public string PenumbraCreateTemporaryCollection(string characterName) - { - if (!CheckPenumbraApi()) return string.Empty; - Logger.Verbose("Creating temp collection for " + characterName); - var ret = _penumbraCreateTemporaryCollection.InvokeFunc("MareSynchronos", characterName, true); - return ret.Item2; - } - public string PenumbraGetMetaManipulations() { if (!CheckPenumbraApi()) return string.Empty; diff --git a/MareSynchronos/Managers/OnlinePlayerManager.cs b/MareSynchronos/Managers/OnlinePlayerManager.cs index 30f0ee9..1dfbed3 100644 --- a/MareSynchronos/Managers/OnlinePlayerManager.cs +++ b/MareSynchronos/Managers/OnlinePlayerManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -19,11 +20,11 @@ public class OnlinePlayerManager : IDisposable private readonly Framework _framework; private readonly IpcManager _ipcManager; private readonly PlayerManager _playerManager; - private readonly List _onlineCachedPlayers = new(); - private readonly Dictionary _temporaryStoredCharacterCache = new(); - private readonly Dictionary _playerTokenDisposal = new(); + private readonly ConcurrentDictionary _onlineCachedPlayers = new(); + private readonly ConcurrentDictionary _temporaryStoredCharacterCache = new(); + private readonly ConcurrentDictionary _playerTokenDisposal = new(); - private List OnlineVisiblePlayerHashes => _onlineCachedPlayers.Where(p => p.PlayerCharacter != null) + private List OnlineVisiblePlayerHashes => _onlineCachedPlayers.Select(p => p.Value).Where(p => p.PlayerCharacter != null) .Select(p => p.PlayerNameHash).ToList(); private DateTime _lastPlayerObjectCheck = DateTime.Now; @@ -58,8 +59,7 @@ public class OnlinePlayerManager : IDisposable private void ApiControllerOnCharacterReceived(object? sender, CharacterReceivedEventArgs e) { - var visiblePlayer = _onlineCachedPlayers.SingleOrDefault(c => c.IsVisible && c.PlayerNameHash == e.CharacterNameHash); - if (visiblePlayer != null) + if (_onlineCachedPlayers.TryGetValue(e.CharacterNameHash, out var visiblePlayer) && visiblePlayer.IsVisible) { Logger.Debug("Received data and applying to " + e.CharacterNameHash); visiblePlayer.ApplyCharacterData(e.CharacterData); @@ -99,7 +99,15 @@ public class OnlinePlayerManager : IDisposable private void IpcManagerOnPenumbraDisposed() { - _onlineCachedPlayers.ForEach(p => p.DisposePlayer()); + DisposePlayers(); + } + + private void DisposePlayers() + { + foreach (var kvp in _onlineCachedPlayers) + { + kvp.Value.DisposePlayer(); + } } private void ApiControllerOnDisconnected() @@ -111,8 +119,11 @@ public class OnlinePlayerManager : IDisposable public void AddInitialPairs(List apiTaskResult) { _onlineCachedPlayers.Clear(); - _onlineCachedPlayers.AddRange(apiTaskResult.Select(CreateCachedPlayer)); - Logger.Verbose("Online and paired users: " + string.Join(Environment.NewLine, _onlineCachedPlayers)); + foreach (var hash in apiTaskResult) + { + _onlineCachedPlayers.TryAdd(hash, CreateCachedPlayer(hash)); + } + Logger.Verbose("Online and paired users: " + string.Join(Environment.NewLine, _onlineCachedPlayers.Select(k => k.Key))); } public void Dispose() @@ -138,7 +149,7 @@ public class OnlinePlayerManager : IDisposable private void RestoreAllCharacters() { - _onlineCachedPlayers.ForEach(p => p.DisposePlayer()); + DisposePlayers(); _onlineCachedPlayers.Clear(); } @@ -171,19 +182,23 @@ public class OnlinePlayerManager : IDisposable private void AddPlayer(string characterNameHash) { - if (_onlineCachedPlayers.Any(p => p.PlayerNameHash == characterNameHash)) + if (_onlineCachedPlayers.TryGetValue(characterNameHash, out var cachedPlayer)) { PushCharacterData(new List() { characterNameHash }); - _playerTokenDisposal.TryGetValue(_onlineCachedPlayers.Single(p => p.PlayerNameHash == characterNameHash), out var cancellationTokenSource); + _playerTokenDisposal.TryGetValue(cachedPlayer, out var cancellationTokenSource); cancellationTokenSource?.Cancel(); return; } - _onlineCachedPlayers.Add(CreateCachedPlayer(characterNameHash)); + _onlineCachedPlayers.TryAdd(characterNameHash, CreateCachedPlayer(characterNameHash)); } private void RemovePlayer(string characterHash) { - var cachedPlayer = _onlineCachedPlayers.First(p => p.PlayerNameHash == characterHash); + if (!_onlineCachedPlayers.TryGetValue(characterHash, out var cachedPlayer)) + { + return; + } + if (_dalamudUtil.IsInGpose) { _playerTokenDisposal.TryGetValue(cachedPlayer, out var cancellationTokenSource); @@ -202,14 +217,14 @@ public class OnlinePlayerManager : IDisposable } cachedPlayer.DisposePlayer(); - _onlineCachedPlayers.RemoveAll(c => c.PlayerNameHash == cachedPlayer.PlayerNameHash); + _onlineCachedPlayers.TryRemove(characterHash, out _); }, token); return; } cachedPlayer.DisposePlayer(); - _onlineCachedPlayers.RemoveAll(c => c.PlayerNameHash == cachedPlayer.PlayerNameHash); + _onlineCachedPlayers.TryRemove(characterHash, out _); } private void FrameworkOnUpdate(Framework framework) @@ -222,22 +237,21 @@ public class OnlinePlayerManager : IDisposable foreach (var pChar in playerCharacters) { var hashedName = Crypto.GetHash256(pChar); - var existingCachedPlayer = _onlineCachedPlayers.SingleOrDefault(p => p.PlayerNameHash == hashedName && !string.IsNullOrEmpty(p.PlayerName)); - if (existingCachedPlayer != null) + if (_onlineCachedPlayers.TryGetValue(hashedName, out var existingPlayer) && !string.IsNullOrEmpty(existingPlayer.PlayerName)) { - existingCachedPlayer.IsVisible = true; + existingPlayer.IsVisible = true; continue; } - if (_temporaryStoredCharacterCache.TryGetValue(hashedName, out var cache)) + if (existingPlayer != null) { - _temporaryStoredCharacterCache.Remove(hashedName); + _temporaryStoredCharacterCache.TryRemove(hashedName, out var cache); + existingPlayer.InitializePlayer(pChar.Address, pChar.Name.ToString(), cache); } - _onlineCachedPlayers.SingleOrDefault(p => p.PlayerNameHash == hashedName)?.InitializePlayer(pChar, cache); } - var newlyVisiblePlayers = _onlineCachedPlayers - .Where(p => p.PlayerCharacter != null && p.IsVisible && !p.WasVisible).Select(p => p.PlayerNameHash) + var newlyVisiblePlayers = _onlineCachedPlayers.Select(v => v.Value) + .Where(p => p.PlayerCharacter != IntPtr.Zero && p.IsVisible && !p.WasVisible).Select(p => p.PlayerNameHash) .ToList(); if (newlyVisiblePlayers.Any()) { diff --git a/MareSynchronos/Managers/PlayerManager.cs b/MareSynchronos/Managers/PlayerManager.cs index feca223..a978fb5 100644 --- a/MareSynchronos/Managers/PlayerManager.cs +++ b/MareSynchronos/Managers/PlayerManager.cs @@ -26,7 +26,6 @@ namespace MareSynchronos.Managers private readonly Dictionary> objectKindsToUpdate = new(); private CancellationTokenSource? _playerChangedCts = new(); - private DateTime _lastPlayerObjectCheck; private List playerRelatedObjects = new List(); @@ -42,7 +41,7 @@ namespace MareSynchronos.Managers _apiController.Connected += ApiControllerOnConnected; _apiController.Disconnected += ApiController_Disconnected; - _dalamudUtil.FrameworkUpdate += DalamudUtilOnFrameworkUpdate; + _dalamudUtil.DelayedFrameworkUpdate += DalamudUtilOnDelayedFrameworkUpdate; Logger.Debug("Watching Player, ApiController is Connected: " + _apiController.IsConnected); if (_apiController.IsConnected) @@ -67,22 +66,18 @@ namespace MareSynchronos.Managers _apiController.Disconnected -= ApiController_Disconnected; _ipcManager.PenumbraRedrawEvent -= IpcManager_PenumbraRedrawEvent; - _dalamudUtil.FrameworkUpdate -= DalamudUtilOnFrameworkUpdate; + _dalamudUtil.DelayedFrameworkUpdate -= DalamudUtilOnDelayedFrameworkUpdate; } - private unsafe void DalamudUtilOnFrameworkUpdate() + private unsafe void DalamudUtilOnDelayedFrameworkUpdate() { if (!_dalamudUtil.IsPlayerPresent || !_ipcManager.Initialized) return; - if (DateTime.Now < _lastPlayerObjectCheck.AddSeconds(0.25)) return; - playerRelatedObjects.ForEach(k => k.CheckAndUpdateObject()); if (playerRelatedObjects.Any(c => c.HasUnprocessedUpdate && !c.IsProcessing)) { OnPlayerOrAttachedObjectsChanged(); } - - _lastPlayerObjectCheck = DateTime.Now; } private void ApiControllerOnConnected() diff --git a/MareSynchronos/Utils/DalamudUtil.cs b/MareSynchronos/Utils/DalamudUtil.cs index 4588bc5..15e14e2 100644 --- a/MareSynchronos/Utils/DalamudUtil.cs +++ b/MareSynchronos/Utils/DalamudUtil.cs @@ -6,7 +6,6 @@ using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.SubKinds; -using Dalamud.Game.ClientState.Objects.Types; using FFXIVClientStructs.FFXIV.Client.Game.Character; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; @@ -27,6 +26,8 @@ namespace MareSynchronos.Utils public event LogIn? LogIn; public event LogOut? LogOut; public event FrameworkUpdate? FrameworkUpdate; + public event FrameworkUpdate? DelayedFrameworkUpdate; + private DateTime _delayedFrameworkUpdateCheck = DateTime.Now; public DalamudUtil(ClientState clientState, ObjectTable objectTable, Framework framework) { @@ -44,7 +45,33 @@ namespace MareSynchronos.Utils private void FrameworkOnUpdate(Framework framework) { - FrameworkUpdate?.Invoke(); + foreach (FrameworkUpdate frameworkInvocation in (FrameworkUpdate?.GetInvocationList() ?? Array.Empty()).Cast()) + { + try + { + frameworkInvocation.Invoke(); + } + catch (Exception ex) + { + Logger.Warn(ex.Message); + Logger.Warn(ex.StackTrace ?? string.Empty); + } + } + + if (DateTime.Now < _delayedFrameworkUpdateCheck.AddSeconds(0.25)) return; + foreach (FrameworkUpdate frameworkInvocation in (DelayedFrameworkUpdate?.GetInvocationList() ?? Array.Empty()).Cast()) + { + try + { + frameworkInvocation.Invoke(); + } + catch (Exception ex) + { + Logger.Warn(ex.Message); + Logger.Warn(ex.StackTrace ?? string.Empty); + } + } + _delayedFrameworkUpdateCheck = DateTime.Now; } private void ClientStateOnLogout(object? sender, EventArgs e)