diff --git a/MareSynchronos/Factories/CharacterDataFactory.cs b/MareSynchronos/Factories/CharacterDataFactory.cs index 6ea9eae..90be2f5 100644 --- a/MareSynchronos/Factories/CharacterDataFactory.cs +++ b/MareSynchronos/Factories/CharacterDataFactory.cs @@ -245,7 +245,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 5d917f1..80b296b 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; } @@ -237,27 +237,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"); @@ -275,7 +275,7 @@ public class CachedPlayer else if (objectKind == ObjectKind.Pet) { int tick = 16; - var pet = _dalamudUtil.GetPet(PlayerCharacter.Address); + var pet = _dalamudUtil.GetPet(PlayerCharacter); if (pet != IntPtr.Zero) { var totalWait = 0; @@ -302,7 +302,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"); @@ -321,7 +321,7 @@ public class CachedPlayer private unsafe void RevertCustomizationData(ObjectKind objectKind) { - if (PlayerCharacter is null) return; + if (PlayerCharacter == IntPtr.Zero) return; if (objectKind == ObjectKind.Player) { @@ -332,12 +332,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); @@ -345,7 +345,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); @@ -353,7 +353,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); @@ -370,12 +370,12 @@ public class CachedPlayer try { Logger.Verbose("Restoring state for " + PlayerName); - _dalamudUtil.FrameworkUpdate -= DalamudUtilOnFrameworkUpdate; + _dalamudUtil.DelayedFrameworkUpdate -= DalamudUtilOnDelayedFrameworkUpdate; _ipcManager.PenumbraRedrawEvent -= IpcManagerOnPenumbraRedrawEvent; _ipcManager.PenumbraRemoveTemporaryCollection(PlayerName); _downloadCancellationTokenSource?.Cancel(); _downloadCancellationTokenSource?.Dispose(); - if (PlayerCharacter != null && PlayerCharacter.IsValid()) + if (PlayerCharacter != IntPtr.Zero) { foreach (var item in _cachedData.FileReplacements) { @@ -391,19 +391,19 @@ 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) { if (!_isDisposed) return; 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, @@ -415,12 +415,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; @@ -437,7 +437,7 @@ public class CachedPlayer public override string ToString() { - return PlayerNameHash + ":" + PlayerName + ":HasChar " + (PlayerCharacter != null); + return PlayerNameHash + ":" + PlayerName + ":HasChar " + (PlayerCharacter != IntPtr.Zero); } private Task? _penumbraRedrawEventTask; @@ -450,10 +450,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) { @@ -473,10 +473,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 9207b60..dcbc3f7 100644 --- a/MareSynchronos/Managers/IpcManager.cs +++ b/MareSynchronos/Managers/IpcManager.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using Dalamud.Game.ClientState.Objects.Types; using MareSynchronos.Utils; using MareSynchronos.WebAPI; -using Lumina.Excel.GeneratedSheets; using Action = System.Action; using System.Collections.Concurrent; @@ -87,14 +86,6 @@ namespace MareSynchronos.Managers _dalamudUtil.FrameworkUpdate += HandleActionQueue; } - private void HandleActionQueue() - { - while (actionQueue.TryDequeue(out var action)) - { - action(); - } - } - private void ResourceLoaded(IntPtr ptr, string arg1, string arg2) { if (ptr != IntPtr.Zero && string.Compare(arg1, arg2, true, System.Globalization.CultureInfo.InvariantCulture) != 0) @@ -104,6 +95,16 @@ namespace MareSynchronos.Managers } } + private void HandleActionQueue() + { + if (actionQueue.TryDequeue(out var action)) + { + if (action == null) return; + Logger.Debug("Execution action in queue: " + action.Method); + action(); + } + } + public event VoidDelegate? PenumbraInitialized; public event VoidDelegate? PenumbraDisposed; public event PenumbraRedrawEvent? PenumbraRedrawEvent; @@ -168,37 +169,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 { @@ -212,14 +226,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 6c28b2e..c06b577 100644 --- a/MareSynchronos/Managers/OnlinePlayerManager.cs +++ b/MareSynchronos/Managers/OnlinePlayerManager.cs @@ -20,11 +20,11 @@ public class OnlinePlayerManager : IDisposable private readonly IpcManager _ipcManager; private readonly PlayerManager _playerManager; private readonly ConcurrentDictionary _onlineCachedPlayers = new(); - private readonly Dictionary _temporaryStoredCharacterCache = new(); - private readonly Dictionary _playerTokenDisposal = new(); + private readonly ConcurrentDictionary _temporaryStoredCharacterCache = new(); + private readonly ConcurrentDictionary _playerTokenDisposal = new(); - private List OnlineVisiblePlayerHashes => _onlineCachedPlayers.Where(p => p.Value.PlayerCharacter != null) - .Select(p => p.Value.PlayerNameHash).ToList(); + private List OnlineVisiblePlayerHashes => _onlineCachedPlayers.Select(p => p.Value).Where(p => p.PlayerCharacter != null) + .Select(p => p.PlayerNameHash).ToList(); private DateTime _lastPlayerObjectCheck = DateTime.Now; public OnlinePlayerManager(Framework framework, ApiController apiController, DalamudUtil dalamudUtil, IpcManager ipcManager, PlayerManager playerManager) @@ -58,8 +58,7 @@ public class OnlinePlayerManager : IDisposable private void ApiControllerOnCharacterReceived(object? sender, CharacterReceivedEventArgs e) { - var visiblePlayer = _onlineCachedPlayers.Select(v => v.Value).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,14 +98,14 @@ public class OnlinePlayerManager : IDisposable private void IpcManagerOnPenumbraDisposed() { - DisposeAllPlayers(); + DisposePlayers(); } - private void DisposeAllPlayers() + private void DisposePlayers() { - foreach (var entry in _onlineCachedPlayers) + foreach (var kvp in _onlineCachedPlayers) { - entry.Value.DisposePlayer(); + kvp.Value.DisposePlayer(); } } @@ -119,11 +118,11 @@ public class OnlinePlayerManager : IDisposable public void AddInitialPairs(List apiTaskResult) { _onlineCachedPlayers.Clear(); - foreach (var result in apiTaskResult) + foreach (var hash in apiTaskResult) { - _onlineCachedPlayers.TryAdd(result, CreateCachedPlayer(result)); + _onlineCachedPlayers.TryAdd(hash, CreateCachedPlayer(hash)); } - Logger.Verbose("Online and paired users: " + string.Join(Environment.NewLine, _onlineCachedPlayers)); + Logger.Verbose("Online and paired users: " + string.Join(Environment.NewLine, _onlineCachedPlayers.Select(k => k.Key))); } public void Dispose() @@ -149,7 +148,7 @@ public class OnlinePlayerManager : IDisposable private void RestoreAllCharacters() { - DisposeAllPlayers(); + DisposePlayers(); _onlineCachedPlayers.Clear(); } @@ -182,10 +181,10 @@ public class OnlinePlayerManager : IDisposable private void AddPlayer(string characterNameHash) { - if (_onlineCachedPlayers.ContainsKey(characterNameHash)) + if (_onlineCachedPlayers.TryGetValue(characterNameHash, out var cachedPlayer)) { PushCharacterData(new List() { characterNameHash }); - _playerTokenDisposal.TryGetValue(_onlineCachedPlayers[characterNameHash], out var cancellationTokenSource); + _playerTokenDisposal.TryGetValue(cachedPlayer, out var cancellationTokenSource); cancellationTokenSource?.Cancel(); return; } @@ -217,7 +216,7 @@ public class OnlinePlayerManager : IDisposable } cachedPlayer.DisposePlayer(); - _onlineCachedPlayers.TryRemove(cachedPlayer.PlayerNameHash, out _); + _onlineCachedPlayers.TryRemove(characterHash, out _); }, token); return; @@ -237,25 +236,21 @@ public class OnlinePlayerManager : IDisposable foreach (var pChar in playerCharacters) { var hashedName = Crypto.GetHash256(pChar); - if (_onlineCachedPlayers.TryGetValue(hashedName, out var existingCachedPlayer) && !string.IsNullOrEmpty(existingCachedPlayer.PlayerName)) + 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); - } - - if (_onlineCachedPlayers.TryGetValue(hashedName, out var playerToInit)) - { - playerToInit.InitializePlayer(pChar, cache); + _temporaryStoredCharacterCache.TryRemove(hashedName, out var cache); + existingPlayer.InitializePlayer(pChar.Address, pChar.Name.ToString(), cache); } } - var newlyVisiblePlayers = _onlineCachedPlayers.Values - .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 ec1ac93..d14e8c6 100644 --- a/MareSynchronos/Managers/PlayerManager.cs +++ b/MareSynchronos/Managers/PlayerManager.cs @@ -28,7 +28,6 @@ namespace MareSynchronos.Managers private readonly Dictionary> objectKindsToUpdate = new(); private CancellationTokenSource? _playerChangedCts = new(); - private DateTime _lastPlayerObjectCheck; private List playerRelatedObjects = new List(); @@ -44,8 +43,8 @@ namespace MareSynchronos.Managers _transientResourceManager = transientResourceManager; _apiController.Connected += ApiControllerOnConnected; _apiController.Disconnected += ApiController_Disconnected; - _dalamudUtil.FrameworkUpdate += DalamudUtilOnFrameworkUpdate; _transientResourceManager.TransientResourceLoaded += HandleTransientResourceLoad; + _dalamudUtil.DelayedFrameworkUpdate += DalamudUtilOnDelayedFrameworkUpdate; Logger.Debug("Watching Player, ApiController is Connected: " + _apiController.IsConnected); if (_apiController.IsConnected) @@ -83,26 +82,22 @@ namespace MareSynchronos.Managers _apiController.Disconnected -= ApiController_Disconnected; _ipcManager.PenumbraRedrawEvent -= IpcManager_PenumbraRedrawEvent; - _dalamudUtil.FrameworkUpdate -= DalamudUtilOnFrameworkUpdate; + _dalamudUtil.DelayedFrameworkUpdate -= DalamudUtilOnDelayedFrameworkUpdate; _transientResourceManager.TransientResourceLoaded -= HandleTransientResourceLoad; _playerChangedCts?.Cancel(); } - 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/UI/CompactUI.cs b/MareSynchronos/UI/CompactUI.cs index 946030e..bbdcbad 100644 --- a/MareSynchronos/UI/CompactUI.cs +++ b/MareSynchronos/UI/CompactUI.cs @@ -36,6 +36,23 @@ namespace MareSynchronos.UI public CompactUi(WindowSystem windowSystem, UiShared uiShared, Configuration configuration, ApiController apiController) : base("###MareSynchronosMainUI") { + +#if DEBUG + string dateTime = "DEV VERSION"; + try + { + dateTime = VariousExtensions.GetLinkerTime(Assembly.GetCallingAssembly()).ToString("yyyyMMddHHmmss"); + } + catch (Exception ex) + { + Logger.Warn("Could not get assembly name"); + Logger.Warn(ex.Message); + Logger.Warn(ex.StackTrace); + } + this.WindowName = "Mare Synchronos " + dateTime + "###MareSynchronosMainUI"; +#else + this.WindowName = "Mare Synchronos " + Assembly.GetExecutingAssembly().GetName().Version; +#endif Logger.Verbose("Creating " + nameof(CompactUi)); #if DEBUG diff --git a/MareSynchronos/Utils/DalamudUtil.cs b/MareSynchronos/Utils/DalamudUtil.cs index 6fe751d..e31e557 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; @@ -30,6 +29,8 @@ namespace MareSynchronos.Utils public event FrameworkUpdate? FrameworkUpdate; public event ClassJobChanged? ClassJobChanged; private uint? classJobId = 0; + public event FrameworkUpdate? DelayedFrameworkUpdate; + private DateTime _delayedFrameworkUpdateCheck = DateTime.Now; public unsafe bool IsGameObjectPresent(IntPtr key) { @@ -61,12 +62,36 @@ namespace MareSynchronos.Utils private void FrameworkOnUpdate(Framework framework) { - if(_clientState.LocalPlayer != null && _clientState.LocalPlayer.ClassJob.Id != classJobId) + foreach (FrameworkUpdate frameworkInvocation in (FrameworkUpdate?.GetInvocationList() ?? Array.Empty()).Cast()) { - classJobId = _clientState.LocalPlayer.ClassJob.Id; - ClassJobChanged?.Invoke(); + try + { + frameworkInvocation.Invoke(); + } + catch (Exception ex) + { + Logger.Warn(ex.Message); + Logger.Warn(ex.StackTrace ?? string.Empty); + } } - FrameworkUpdate?.Invoke(); + + classJobId = _clientState.LocalPlayer.ClassJob.Id; + ClassJobChanged?.Invoke(); + + 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)