From a618fad7d9d62d55185ea2afd227224636954226 Mon Sep 17 00:00:00 2001 From: Stanley Dimant Date: Sun, 11 Sep 2022 01:23:41 +0200 Subject: [PATCH] 0.4.2: fix heels integration, potentially fix crashes, delay handling of transient resource loads, change transients to concurrent dictionary --- .../Factories/CharacterDataFactory.cs | 113 +++++++++--------- MareSynchronos/Managers/CachedPlayer.cs | 12 ++ MareSynchronos/Managers/IpcManager.cs | 36 ++++-- MareSynchronos/Managers/PlayerManager.cs | 26 ++-- .../Managers/TransientResourceManager.cs | 32 +++-- MareSynchronos/MareSynchronos.csproj | 2 +- MareSynchronos/Models/FileReplacement.cs | 2 +- MareSynchronos/Models/PlayerRelatedObject.cs | 3 +- 8 files changed, 142 insertions(+), 84 deletions(-) diff --git a/MareSynchronos/Factories/CharacterDataFactory.cs b/MareSynchronos/Factories/CharacterDataFactory.cs index d912ef8..3fe0343 100644 --- a/MareSynchronos/Factories/CharacterDataFactory.cs +++ b/MareSynchronos/Factories/CharacterDataFactory.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; -using System.Threading.Tasks; -using Dalamud.Game; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; @@ -40,7 +38,7 @@ public class CharacterDataFactory return playerPointer == IntPtr.Zero || ((Character*)playerPointer)->GameObject.GetDrawObject() == null; } - public CharacterData BuildCharacterData(CharacterData previousData, ObjectKind objectKind, IntPtr playerPointer, CancellationToken token) + public CharacterData BuildCharacterData(CharacterData previousData, PlayerRelatedObject playerRelatedObject, CancellationToken token) { if (!_ipcManager.Initialized) { @@ -50,20 +48,20 @@ public class CharacterDataFactory bool pointerIsZero = true; try { - pointerIsZero = CheckForPointer(playerPointer); + pointerIsZero = CheckForPointer(playerRelatedObject.Address); } catch (Exception ex) { - Logger.Warn("Could not create data for " + objectKind); + Logger.Warn("Could not create data for " + playerRelatedObject.ObjectKind); Logger.Warn(ex.Message); Logger.Warn(ex.StackTrace ?? string.Empty); } if (pointerIsZero) { - Logger.Verbose("Pointer was zero for " + objectKind); - previousData.FileReplacements.Remove(objectKind); - previousData.GlamourerString.Remove(objectKind); + Logger.Verbose("Pointer was zero for " + playerRelatedObject.ObjectKind); + previousData.FileReplacements.Remove(playerRelatedObject.ObjectKind); + previousData.GlamourerString.Remove(playerRelatedObject.ObjectKind); return previousData; } @@ -72,7 +70,7 @@ public class CharacterDataFactory try { - return CreateCharacterData(previousData, objectKind, playerPointer, token); + return CreateCharacterData(previousData, playerRelatedObject, token); } catch (OperationCanceledException) { @@ -80,7 +78,7 @@ public class CharacterDataFactory } catch (Exception e) { - Logger.Warn("Failed to create " + objectKind + " data"); + Logger.Warn("Failed to create " + playerRelatedObject.ObjectKind + " data"); Logger.Warn(e.Message); Logger.Warn(e.StackTrace ?? string.Empty); } @@ -232,66 +230,75 @@ public class CharacterDataFactory cache.AddFileReplacement(objectKind, texDx11Replacement); } - private unsafe CharacterData CreateCharacterData(CharacterData previousData, ObjectKind objectKind, IntPtr charaPointer, CancellationToken token) + private unsafe CharacterData CreateCharacterData(CharacterData previousData, PlayerRelatedObject playerRelatedObject, CancellationToken token) { - if (previousData.FileReplacements.ContainsKey(objectKind)) - { - previousData.FileReplacements[objectKind].Clear(); - } - else - { - previousData.FileReplacements.Add(objectKind, new()); - } - - var chara = _dalamudUtil.CreateGameObject(charaPointer)!; - while (!_dalamudUtil.IsObjectPresent(chara)) - { - Logger.Verbose("Character is null but it shouldn't be, waiting"); - Thread.Sleep(50); - } - - _dalamudUtil.WaitWhileCharacterIsDrawing(objectKind.ToString(), charaPointer); + var objectKind = playerRelatedObject.ObjectKind; + var charaPointer = playerRelatedObject.Address; Stopwatch st = Stopwatch.StartNew(); - previousData.ManipulationString = _ipcManager.PenumbraGetMetaManipulations(); - - previousData.GlamourerString[objectKind] = _ipcManager.GlamourerGetCharacterCustomization(charaPointer); - - var human = (Human*)((Character*)charaPointer)->GameObject.GetDrawObject(); - for (var mdlIdx = 0; mdlIdx < human->CharacterBase.SlotCount; ++mdlIdx) + if (playerRelatedObject.HasUnprocessedUpdate) { - var mdl = (RenderModel*)human->CharacterBase.ModelArray[mdlIdx]; - if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) + Logger.Debug("Handling unprocessed update for " + objectKind); + + if (previousData.FileReplacements.ContainsKey(objectKind)) { - continue; + previousData.FileReplacements[objectKind].Clear(); + } + else + { + previousData.FileReplacements.Add(objectKind, new()); } - token.ThrowIfCancellationRequested(); + var chara = _dalamudUtil.CreateGameObject(charaPointer)!; + while (!_dalamudUtil.IsObjectPresent(chara)) + { + Logger.Verbose("Character is null but it shouldn't be, waiting"); + Thread.Sleep(50); + } - AddReplacementsFromRenderModel(mdl, objectKind, previousData, 0); - } + _dalamudUtil.WaitWhileCharacterIsDrawing(objectKind.ToString(), charaPointer); - foreach (var item in previousData.FileReplacements[objectKind]) - { - transientResourceManager.RemoveTransientResource(charaPointer, item); - } + var human = (Human*)((Character*)charaPointer)->GameObject.GetDrawObject(); + for (var mdlIdx = 0; mdlIdx < human->CharacterBase.SlotCount; ++mdlIdx) + { + var mdl = (RenderModel*)human->CharacterBase.ModelArray[mdlIdx]; + if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) + { + continue; + } - if (objectKind == ObjectKind.Player) - { - AddPlayerSpecificReplacements(previousData, objectKind, charaPointer, human); - } + token.ThrowIfCancellationRequested(); + + AddReplacementsFromRenderModel(mdl, objectKind, previousData, 0); + } - if (objectKind == ObjectKind.Pet) - { foreach (var item in previousData.FileReplacements[objectKind]) { - transientResourceManager.AddSemiTransientResource(objectKind, item); + transientResourceManager.RemoveTransientResource(charaPointer, item); } - previousData.FileReplacements[objectKind].Clear(); + if (objectKind == ObjectKind.Player) + { + AddPlayerSpecificReplacements(previousData, objectKind, charaPointer, human); + } + + if (objectKind == ObjectKind.Pet) + { + foreach (var item in previousData.FileReplacements[objectKind]) + { + transientResourceManager.AddSemiTransientResource(objectKind, item); + } + + previousData.FileReplacements[objectKind].Clear(); + } } + previousData.ManipulationString = _ipcManager.PenumbraGetMetaManipulations(); + previousData.GlamourerString[objectKind] = _ipcManager.GlamourerGetCharacterCustomization(charaPointer); + previousData.HeelsOffset = _ipcManager.GetHeelsOffset(); + + Logger.Debug("Handling transient update for " + objectKind); ManageSemiTransientData(previousData, objectKind, charaPointer); st.Stop(); @@ -387,8 +394,6 @@ public class CharacterDataFactory { transientResourceManager.RemoveTransientResource(charaPointer, item); } - - previousData.HeelsOffset = _ipcManager.GetHeelsOffset(); } private void AddReplacementSkeleton(ushort raceSexId, ObjectKind objectKind, CharacterData cache) diff --git a/MareSynchronos/Managers/CachedPlayer.cs b/MareSynchronos/Managers/CachedPlayer.cs index 5c6ebd9..527ec39 100644 --- a/MareSynchronos/Managers/CachedPlayer.cs +++ b/MareSynchronos/Managers/CachedPlayer.cs @@ -117,6 +117,17 @@ public class CachedPlayer continue; } } + + if (objectKind == ObjectKind.Player) + { + bool heelsOffsetDifferent = _cachedData.HeelsOffset != characterData.HeelsOffset; + if (heelsOffsetDifferent) + { + Logger.Debug("Updating " + objectKind); + charaDataToUpdate.Add(objectKind); + continue; + } + } } _cachedData = characterData; @@ -324,6 +335,7 @@ public class CachedPlayer { _ipcManager.GlamourerApplyOnlyCustomization(_originalGlamourerData, PlayerCharacter); _ipcManager.GlamourerApplyOnlyEquipment(_lastGlamourerData, PlayerCharacter); + _ipcManager.HeelsRestoreOffsetForPlayer(PlayerCharacter); } else { diff --git a/MareSynchronos/Managers/IpcManager.cs b/MareSynchronos/Managers/IpcManager.cs index 4500101..f98a6d1 100644 --- a/MareSynchronos/Managers/IpcManager.cs +++ b/MareSynchronos/Managers/IpcManager.cs @@ -207,16 +207,30 @@ namespace MareSynchronos.Managers }); } + public void HeelsRestoreOffsetForPlayer(IntPtr character) + { + if (!CheckHeelsApi()) return; + actionQueue.Enqueue(() => + { + var gameObj = _dalamudUtil.CreateGameObject(character); + if (gameObj != null) + { + Logger.Verbose("Restoring Heels data to " + character.ToString("X")); + _heelsUnregisterPlayer.InvokeAction(gameObj); + } + }); + } + public void GlamourerApplyAll(string? customization, IntPtr obj) { if (!CheckGlamourerApi() || string.IsNullOrEmpty(customization)) return; actionQueue.Enqueue(() => { var gameObj = _dalamudUtil.CreateGameObject(obj); - if (gameObj != null) + if (gameObj is Character c) { - Logger.Verbose("Glamourer applying for " + gameObj); - _glamourerApplyAll!.InvokeAction(customization, gameObj); + Logger.Verbose("Glamourer applying for " + c.Address.ToString("X")); + _glamourerApplyAll!.InvokeAction(customization, c); } }); } @@ -227,10 +241,10 @@ namespace MareSynchronos.Managers actionQueue.Enqueue(() => { var gameObj = _dalamudUtil.CreateGameObject(character); - if (gameObj != null) + if (gameObj is Character c) { - Logger.Verbose("Glamourer apply only equipment to " + character.ToString("X")); - _glamourerApplyOnlyEquipment!.InvokeAction(customization, gameObj); + Logger.Verbose("Glamourer apply only equipment to " + c.Address.ToString("X")); + _glamourerApplyOnlyEquipment!.InvokeAction(customization, c); } }); } @@ -241,10 +255,10 @@ namespace MareSynchronos.Managers actionQueue.Enqueue(() => { var gameObj = _dalamudUtil.CreateGameObject(character); - if (gameObj != null) + if (gameObj is Character c) { - Logger.Verbose("Glamourer apply only customization to " + character.ToString("X")); - _glamourerApplyOnlyCustomization!.InvokeAction(customization, gameObj); + Logger.Verbose("Glamourer apply only customization to " + c.Address.ToString("X")); + _glamourerApplyOnlyCustomization!.InvokeAction(customization, c); } }); } @@ -255,9 +269,9 @@ namespace MareSynchronos.Managers try { var gameObj = _dalamudUtil.CreateGameObject(character); - if (gameObj != null) + if (gameObj is Character c) { - var glamourerString = _glamourerGetAllCustomization!.InvokeFunc(gameObj); + var glamourerString = _glamourerGetAllCustomization!.InvokeFunc(c); byte[] bytes = Convert.FromBase64String(glamourerString); // ignore transparency bytes[88] = 128; diff --git a/MareSynchronos/Managers/PlayerManager.cs b/MareSynchronos/Managers/PlayerManager.cs index 582aee3..b65caa5 100644 --- a/MareSynchronos/Managers/PlayerManager.cs +++ b/MareSynchronos/Managers/PlayerManager.cs @@ -28,6 +28,7 @@ namespace MareSynchronos.Managers private readonly Dictionary> objectKindsToUpdate = new(); private CancellationTokenSource? _playerChangedCts = new(); + private CancellationTokenSource _transientUpdateCts = new(); private List playerRelatedObjects = new List(); @@ -69,8 +70,19 @@ namespace MareSynchronos.Managers { if (obj.Address == gameObj && !obj.HasUnprocessedUpdate) { - obj.HasUnprocessedUpdate = true; - OnPlayerOrAttachedObjectsChanged(); + _transientUpdateCts.Cancel(); + _transientUpdateCts = new CancellationTokenSource(); + var token = _transientUpdateCts.Token; + Task.Run(async () => + { + Logger.Debug("Delaying transient resource load update"); + await Task.Delay(750, token); + if (obj.HasUnprocessedUpdate || token.IsCancellationRequested) return; + Logger.Debug("Firing transient resource load update"); + obj.HasTransientsUpdate = true; + OnPlayerOrAttachedObjectsChanged(); + }, token); + return; } } @@ -82,7 +94,7 @@ namespace MareSynchronos.Managers if (LastCreatedCharacterData != null && LastCreatedCharacterData.HeelsOffset != change && !player.IsProcessing) { Logger.Debug("Heels offset changed to " + change); - playerRelatedObjects.First(f => f.ObjectKind == ObjectKind.Player).HasUnprocessedUpdate = true; + playerRelatedObjects.First(f => f.ObjectKind == ObjectKind.Player).HasTransientsUpdate = true; } } @@ -129,14 +141,15 @@ namespace MareSynchronos.Managers private async Task CreateFullCharacterCacheDto(CancellationToken token) { - foreach (var unprocessedObject in playerRelatedObjects.Where(c => c.HasUnprocessedUpdate).ToList()) + foreach (var unprocessedObject in playerRelatedObjects.Where(c => c.HasUnprocessedUpdate || c.HasTransientsUpdate).ToList()) { Logger.Verbose("Building Cache for " + unprocessedObject.ObjectKind); - PermanentDataCache = _characterDataFactory.BuildCharacterData(PermanentDataCache, unprocessedObject.ObjectKind, unprocessedObject.Address, token); + PermanentDataCache = _characterDataFactory.BuildCharacterData(PermanentDataCache, unprocessedObject, token); if (!token.IsCancellationRequested) { unprocessedObject.HasUnprocessedUpdate = false; unprocessedObject.IsProcessing = false; + unprocessedObject.HasTransientsUpdate = false; } token.ThrowIfCancellationRequested(); } @@ -165,7 +178,6 @@ namespace MareSynchronos.Managers if (address == item.Address) { Logger.Debug("Penumbra redraw Event for " + item.ObjectKind); - //_transientResourceManager.CleanSemiTransientResources(item.ObjectKind); item.HasUnprocessedUpdate = true; } } @@ -178,7 +190,7 @@ namespace MareSynchronos.Managers private void OnPlayerOrAttachedObjectsChanged() { - var unprocessedObjects = playerRelatedObjects.Where(c => c.HasUnprocessedUpdate).ToList(); + var unprocessedObjects = playerRelatedObjects.Where(c => c.HasUnprocessedUpdate || c.HasTransientsUpdate).ToList(); foreach (var unprocessedObject in unprocessedObjects) { unprocessedObject.IsProcessing = true; diff --git a/MareSynchronos/Managers/TransientResourceManager.cs b/MareSynchronos/Managers/TransientResourceManager.cs index 7bc3690..ac7410c 100644 --- a/MareSynchronos/Managers/TransientResourceManager.cs +++ b/MareSynchronos/Managers/TransientResourceManager.cs @@ -2,8 +2,11 @@ using MareSynchronos.Models; using MareSynchronos.Utils; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace MareSynchronos.Managers { @@ -16,8 +19,9 @@ namespace MareSynchronos.Managers public event TransientResourceLoadedEvent? TransientResourceLoaded; - private Dictionary> TransientResources { get; } = new(); - private Dictionary> SemiTransientResources { get; } = new(); + private ConcurrentDictionary> TransientResources { get; } = new(); + private ConcurrentDictionary> SemiTransientResources { get; } = new(); + private CancellationTokenSource transientInvokeDelayCts = new CancellationTokenSource(); public TransientResourceManager(IpcManager manager, DalamudUtil dalamudUtil) { manager.PenumbraResourceLoadEvent += Manager_PenumbraResourceLoadEvent; @@ -42,7 +46,7 @@ namespace MareSynchronos.Managers if (!dalamudUtil.IsGameObjectPresent(item.Key)) { Logger.Debug("Object not present anymore: " + item.Key.ToString("X")); - TransientResources.Remove(item.Key); + TransientResources.TryRemove(item.Key, out _); } } } @@ -138,13 +142,23 @@ namespace MareSynchronos.Managers SemiTransientResources[objectKind].RemoveWhere(f => f.GamePaths.First().ToLowerInvariant() == item.ToLowerInvariant()); } - if (!SemiTransientResources[objectKind].Any(f => f.GamePaths.First().ToLowerInvariant() == item.ToLowerInvariant())) + try { - Logger.Debug("Persisting " + item.ToLowerInvariant()); - var fileReplacement = createFileReplacement(item.ToLowerInvariant(), true); - if (!fileReplacement.HasFileReplacement) - fileReplacement = createFileReplacement(item.ToLowerInvariant(), false); - SemiTransientResources[objectKind].Add(fileReplacement); + if (!SemiTransientResources[objectKind].Any(f => f.GamePaths.First().ToLowerInvariant() == item.ToLowerInvariant())) + { + Logger.Debug("Persisting " + item.ToLowerInvariant()); + + var fileReplacement = createFileReplacement(item.ToLowerInvariant(), true); + if (!fileReplacement.HasFileReplacement) + fileReplacement = createFileReplacement(item.ToLowerInvariant(), false); + SemiTransientResources[objectKind].Add(fileReplacement); + } + } + catch (Exception ex) + { + Logger.Warn("Issue during transient file persistence"); + Logger.Warn(ex.Message); + Logger.Warn(ex.StackTrace.ToString()); } } diff --git a/MareSynchronos/MareSynchronos.csproj b/MareSynchronos/MareSynchronos.csproj index e95f6c2..0550ecb 100644 --- a/MareSynchronos/MareSynchronos.csproj +++ b/MareSynchronos/MareSynchronos.csproj @@ -3,7 +3,7 @@ - 0.4.1 + 0.4.2 https://github.com/Penumbra-Sync/client diff --git a/MareSynchronos/Models/FileReplacement.cs b/MareSynchronos/Models/FileReplacement.cs index 3d82d00..bf6a3ee 100644 --- a/MareSynchronos/Models/FileReplacement.cs +++ b/MareSynchronos/Models/FileReplacement.cs @@ -35,7 +35,7 @@ namespace MareSynchronos.Models public void SetResolvedPath(string path) { - ResolvedPath = path.ToLowerInvariant().Replace('/', '\\').Replace(_penumbraDirectory, "").Replace('\\', '/'); + ResolvedPath = path.ToLowerInvariant();//.Replace('/', '\\').Replace(_penumbraDirectory, "").Replace('\\', '/'); if (!HasFileReplacement || IsFileSwap) return; _ = Task.Run(() => diff --git a/MareSynchronos/Models/PlayerRelatedObject.cs b/MareSynchronos/Models/PlayerRelatedObject.cs index 34b6ad5..5b135fd 100644 --- a/MareSynchronos/Models/PlayerRelatedObject.cs +++ b/MareSynchronos/Models/PlayerRelatedObject.cs @@ -7,7 +7,7 @@ using Penumbra.GameData.ByteString; namespace MareSynchronos.Models { - internal class PlayerRelatedObject + public class PlayerRelatedObject { private readonly Func getAddress; @@ -46,6 +46,7 @@ namespace MareSynchronos.Models public byte? HatState { get; set; } public byte? VisorWeaponState { get; set; } + public bool HasTransientsUpdate { get; set; } = false; public bool HasUnprocessedUpdate { get; set; } = false; public bool DoNotSendUpdate { get; set; } = false; public bool IsProcessing { get; set; } = false;