From f39c085fbfd32ae473bc6f8bea3eeaaf1b034d05 Mon Sep 17 00:00:00 2001 From: Stanley Dimant Date: Sun, 24 Jul 2022 14:34:26 +0200 Subject: [PATCH] adjust to send only one packet for all changes at once --- MareAPI | 2 +- .../Factories/CharacterDataFactory.cs | 103 +++++---- MareSynchronos/Managers/CachedPlayer.cs | 140 ++++++++---- MareSynchronos/Managers/IpcManager.cs | 12 +- .../Managers/OnlinePlayerManager.cs | 20 +- MareSynchronos/Managers/PlayerManager.cs | 200 ++++++------------ MareSynchronos/Models/CharacterData.cs | 24 +-- MareSynchronos/Models/PlayerAttachedObject.cs | 51 ++++- .../WebAPI/ApIController.Functions.Files.cs | 22 +- .../WebAPI/ApiController.Connectivity.cs | 12 +- 10 files changed, 300 insertions(+), 286 deletions(-) diff --git a/MareAPI b/MareAPI index 96d17b0..a649b36 160000 --- a/MareAPI +++ b/MareAPI @@ -1 +1 @@ -Subproject commit 96d17b006e58614228f00a361b60f569b6e99445 +Subproject commit a649b364955ce431f41c53c016823763febde708 diff --git a/MareSynchronos/Factories/CharacterDataFactory.cs b/MareSynchronos/Factories/CharacterDataFactory.cs index c17c74c..6d5a2a3 100644 --- a/MareSynchronos/Factories/CharacterDataFactory.cs +++ b/MareSynchronos/Factories/CharacterDataFactory.cs @@ -7,6 +7,7 @@ using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource; +using MareSynchronos.API; using MareSynchronos.Interop; using MareSynchronos.Managers; using MareSynchronos.Models; @@ -30,7 +31,7 @@ public class CharacterDataFactory _ipcManager = ipcManager; } - public CharacterData? BuildCharacterData(IntPtr playerPointer) + public CharacterData BuildCharacterData(CharacterData previousData, ObjectKind objectKind, IntPtr playerPointer) { if (!_ipcManager.Initialized) { @@ -39,19 +40,22 @@ public class CharacterDataFactory if (playerPointer == IntPtr.Zero) { - return null; + Logger.Verbose("Pointer was zero for " + objectKind); + previousData.FileReplacements.Remove(objectKind); + previousData.GlamourerString.Remove(objectKind); + return previousData; } try { - return CreateCharacterData(playerPointer); + return CreateCharacterData(previousData, objectKind, playerPointer); } catch (Exception e) { - Logger.Warn("Failed to create character data"); + Logger.Warn("Failed to create " + objectKind + " data"); Logger.Warn(e.Message); Logger.Warn(e.StackTrace ?? string.Empty); - return null; + return previousData; } } @@ -60,16 +64,15 @@ public class CharacterDataFactory return (string.Join("", Enumerable.Repeat("\t", inheritanceLevel)), string.Join("", Enumerable.Repeat("\t", inheritanceLevel + 2))); } - private void DebugPrint(FileReplacement fileReplacement, string objectKind, string resourceType, int inheritanceLevel) + private void DebugPrint(FileReplacement fileReplacement, ObjectKind objectKind, string resourceType, int inheritanceLevel) { var indentation = GetIndentationForInheritanceLevel(inheritanceLevel); - objectKind += string.IsNullOrEmpty(objectKind) ? "" : " "; Logger.Verbose(indentation.Item1 + objectKind + resourceType + " [" + string.Join(", ", fileReplacement.GamePaths) + "]"); Logger.Verbose(indentation.Item2 + "=> " + fileReplacement.ResolvedPath); } - private unsafe void AddReplacementsFromRenderModel(RenderModel* mdl, CharacterData cache, int inheritanceLevel = 0, string objectKind = "") + private unsafe void AddReplacementsFromRenderModel(RenderModel* mdl, ObjectKind objectKind, CharacterData cache, int inheritanceLevel = 0) { if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) { @@ -91,18 +94,18 @@ public class CharacterDataFactory FileReplacement mdlFileReplacement = CreateFileReplacement(mdlPath); DebugPrint(mdlFileReplacement, objectKind, "Model", inheritanceLevel); - cache.AddFileReplacement(mdlFileReplacement); + cache.AddFileReplacement(objectKind, mdlFileReplacement); for (var mtrlIdx = 0; mtrlIdx < mdl->MaterialCount; mtrlIdx++) { var mtrl = (Material*)mdl->Materials[mtrlIdx]; if (mtrl == null) continue; - AddReplacementsFromMaterial(mtrl, cache, inheritanceLevel + 1, objectKind); + AddReplacementsFromMaterial(mtrl, objectKind, cache, inheritanceLevel + 1); } } - private unsafe void AddReplacementsFromMaterial(Material* mtrl, CharacterData cache, int inheritanceLevel = 0, string objectKind = "") + private unsafe void AddReplacementsFromMaterial(Material* mtrl, ObjectKind objectKind, CharacterData cache, int inheritanceLevel = 0) { string fileName; try @@ -122,7 +125,7 @@ public class CharacterDataFactory var mtrlFileReplacement = CreateFileReplacement(mtrlPath); DebugPrint(mtrlFileReplacement, objectKind, "Material", inheritanceLevel); - cache.AddFileReplacement(mtrlFileReplacement); + cache.AddFileReplacement(objectKind, mtrlFileReplacement); var mtrlResourceHandle = (MtrlResource*)mtrl->ResourceHandle; for (var resIdx = 0; resIdx < mtrlResourceHandle->NumTex; resIdx++) @@ -131,11 +134,11 @@ public class CharacterDataFactory if (string.IsNullOrEmpty(texPath)) continue; - AddReplacementsFromTexture(texPath, cache, inheritanceLevel + 1, objectKind); + AddReplacementsFromTexture(texPath, objectKind, cache, inheritanceLevel + 1); } } - private void AddReplacementsFromTexture(string texPath, CharacterData cache, int inheritanceLevel = 0, string objectKind = "", bool doNotReverseResolve = true) + private void AddReplacementsFromTexture(string texPath, ObjectKind objectKind, CharacterData cache, int inheritanceLevel = 0, bool doNotReverseResolve = true) { if (texPath.IsNullOrEmpty()) return; @@ -144,7 +147,7 @@ public class CharacterDataFactory var texFileReplacement = CreateFileReplacement(texPath, doNotReverseResolve); DebugPrint(texFileReplacement, objectKind, "Texture", inheritanceLevel); - cache.AddFileReplacement(texFileReplacement); + cache.AddFileReplacement(objectKind, texFileReplacement); if (texPath.Contains("/--")) return; @@ -153,10 +156,10 @@ public class CharacterDataFactory DebugPrint(texDx11Replacement, objectKind, "Texture (DX11)", inheritanceLevel); - cache.AddFileReplacement(texDx11Replacement); + cache.AddFileReplacement(objectKind, texDx11Replacement); } - private unsafe CharacterData CreateCharacterData(IntPtr charaPointer) + private unsafe CharacterData CreateCharacterData(CharacterData previousData, ObjectKind objectKind, IntPtr charaPointer) { Stopwatch st = Stopwatch.StartNew(); var chara = _dalamudUtil.CreateGameObject(charaPointer)!; @@ -166,20 +169,18 @@ public class CharacterDataFactory Thread.Sleep(50); } _dalamudUtil.WaitWhileCharacterIsDrawing(charaPointer); - var cache = new CharacterData() - { - ManipulationString = _ipcManager.PenumbraGetMetaManipulations(), - }; - try + if (previousData.FileReplacements.ContainsKey(objectKind)) { - cache.GlamourerString = _ipcManager.GlamourerGetCharacterCustomization(chara); - } - catch - { - // might not have glamourer data + previousData.FileReplacements[objectKind].Clear(); } + previousData.ManipulationString = _ipcManager.PenumbraGetMetaManipulations(); + + if (objectKind is not ObjectKind.Mount) + { + previousData.GlamourerString[objectKind] = _ipcManager.GlamourerGetCharacterCustomization(chara); + } var human = (Human*)((Character*)charaPointer)->GameObject.GetDrawObject(); for (var mdlIdx = 0; mdlIdx < human->CharacterBase.SlotCount; ++mdlIdx) @@ -190,54 +191,50 @@ public class CharacterDataFactory continue; } - AddReplacementsFromRenderModel(mdl, cache, 0, "Character"); + AddReplacementsFromRenderModel(mdl, objectKind, previousData, 0); } - var weaponObject = (Weapon*)((Object*)human)->ChildObject; - - if ((IntPtr)weaponObject != IntPtr.Zero) + if (objectKind == ObjectKind.Player) { - var mainHandWeapon = weaponObject->WeaponRenderModel->RenderModel; + var weaponObject = (Weapon*)((Object*)human)->ChildObject; - AddReplacementsFromRenderModel(mainHandWeapon, cache, 0, "Weapon"); - - if (weaponObject->NextSibling != (IntPtr)weaponObject) + if ((IntPtr)weaponObject != IntPtr.Zero) { - var offHandWeapon = ((Weapon*)weaponObject->NextSibling)->WeaponRenderModel->RenderModel; + var mainHandWeapon = weaponObject->WeaponRenderModel->RenderModel; - AddReplacementsFromRenderModel(offHandWeapon, cache, 1, "OffHand Weapon"); - } - } + AddReplacementsFromRenderModel(mainHandWeapon, objectKind, previousData, 0); - if (!string.IsNullOrEmpty(cache.GlamourerString)) - { - try - { - AddReplacementSkeleton(((HumanExt*)human)->Human.RaceSexId, cache); - AddReplacementsFromTexture(new Utf8String(((HumanExt*)human)->Decal->FileName()).ToString(), cache, 0, "Decal", false); - AddReplacementsFromTexture(new Utf8String(((HumanExt*)human)->LegacyBodyDecal->FileName()).ToString(), cache, 0, "Legacy Decal", false); + if (weaponObject->NextSibling != (IntPtr)weaponObject) + { + var offHandWeapon = ((Weapon*)weaponObject->NextSibling)->WeaponRenderModel->RenderModel; + + AddReplacementsFromRenderModel(offHandWeapon, objectKind, previousData, 1); + } } - catch { } + + AddReplacementSkeleton(((HumanExt*)human)->Human.RaceSexId, objectKind, previousData); + AddReplacementsFromTexture(new Utf8String(((HumanExt*)human)->Decal->FileName()).ToString(), objectKind, previousData, 0, false); + AddReplacementsFromTexture(new Utf8String(((HumanExt*)human)->LegacyBodyDecal->FileName()).ToString(), objectKind, previousData, 0, false); } st.Stop(); - Logger.Verbose("Building Character Data took " + st.Elapsed); + Logger.Verbose("Building " + objectKind + " Data took " + st.Elapsed); - return cache; + return previousData; } - private void AddReplacementSkeleton(ushort raceSexId, CharacterData cache) + private void AddReplacementSkeleton(ushort raceSexId, ObjectKind objectKind, CharacterData cache) { string raceSexIdString = raceSexId.ToString("0000"); string skeletonPath = $"chara/human/c{raceSexIdString}/skeleton/base/b0001/skl_c{raceSexIdString}b0001.sklb"; - Logger.Verbose("Adding File Replacement for Skeleton " + skeletonPath); + //Logger.Verbose("Adding File Replacement for Skeleton " + skeletonPath); var replacement = CreateFileReplacement(skeletonPath, true); - cache.AddFileReplacement(replacement); + cache.AddFileReplacement(objectKind, replacement); - DebugPrint(replacement, "Skeleton", "SKLB", 0); + DebugPrint(replacement, objectKind, "SKLB", 0); } private FileReplacement CreateFileReplacement(string path, bool doNotReverseResolve = false) diff --git a/MareSynchronos/Managers/CachedPlayer.cs b/MareSynchronos/Managers/CachedPlayer.cs index 671899a..ccbdf3a 100644 --- a/MareSynchronos/Managers/CachedPlayer.cs +++ b/MareSynchronos/Managers/CachedPlayer.cs @@ -42,7 +42,7 @@ public class CachedPlayer } private bool _isDisposed = false; - private Dictionary _downloadCancellationTokenSources = new(); + private CancellationTokenSource? _downloadCancellationTokenSource = new(); private string _lastGlamourerData = string.Empty; @@ -58,7 +58,7 @@ public class CachedPlayer public bool WasVisible { get; private set; } - private readonly Dictionary _cache = new(); + private CharacterCacheDto _cachedData = new(); private CharacterEquipment? _currentCharacterEquipment; @@ -67,33 +67,41 @@ public class CachedPlayer Logger.Debug("Received data for " + this); Logger.Debug("Checking for files to download for player " + PlayerName); - Logger.Debug("Hash for data is " + characterData.Hash); + Logger.Debug("Hash for data is " + characterData.GetHashCode()); - if (!_cache.ContainsKey(characterData.ObjectKind)) + if (characterData.GetHashCode() == _cachedData.GetHashCode()) return; + + List charaDataToUpdate = new List(); + foreach (var objectKind in Enum.GetValues()) { - _cache.Add(characterData.ObjectKind, new()); - _downloadCancellationTokenSources.Add(characterData.ObjectKind, null); + bool doesntContainKey = !_cachedData.FileReplacements.ContainsKey(objectKind) + || (_cachedData.FileReplacements.ContainsKey(objectKind) && !characterData.FileReplacements.ContainsKey(objectKind)); + if (doesntContainKey) + { + charaDataToUpdate.Add(objectKind); + continue; + } + + bool listsAreEqual = Enumerable.SequenceEqual(_cachedData.FileReplacements[objectKind], characterData.FileReplacements[objectKind]); + bool glamourerDataDifferent = _cachedData.GlamourerData[objectKind] != characterData.GlamourerData[objectKind]; + if (!listsAreEqual || glamourerDataDifferent) + { + Logger.Debug("Updating " + objectKind); + + charaDataToUpdate.Add(objectKind); + } } - _cache.TryGetValue(characterData.ObjectKind, out var cachedDto); - if ((cachedDto?.Hash ?? string.Empty) != characterData.Hash) - { - Logger.Debug("Received total " + characterData.FileReplacements.Count + " file replacement data"); - _cache[characterData.ObjectKind] = characterData; - } - else - { - Logger.Debug("Had valid local cache for " + PlayerName); - } + _cachedData = characterData; - DownloadAndApplyCharacter(characterData.ObjectKind); + DownloadAndApplyCharacter(charaDataToUpdate); } - private void DownloadAndApplyCharacter(ObjectKind objectKind) + private void DownloadAndApplyCharacter(List objectKind) { - _downloadCancellationTokenSources[objectKind]?.Cancel(); - _downloadCancellationTokenSources[objectKind] = new CancellationTokenSource(); - var downloadToken = _downloadCancellationTokenSources[objectKind]!.Token; + _downloadCancellationTokenSource?.Cancel(); + _downloadCancellationTokenSource = new CancellationTokenSource(); + var downloadToken = _downloadCancellationTokenSource.Token; var downloadId = _apiController.GetDownloadId(); Task.Run(async () => { @@ -103,7 +111,7 @@ public class CachedPlayer int attempts = 0; while ((toDownloadReplacements = TryCalculateModdedDictionary(out moddedPaths)).Count > 0 && attempts++ <= 10) { - Logger.Debug("Downloading missing files for player " + PlayerName); + Logger.Debug("Downloading missing files for player " + PlayerName + ", kind: " + objectKind); await _apiController.DownloadFiles(downloadId, toDownloadReplacements, downloadToken); if (downloadToken.IsCancellationRequested) { @@ -127,7 +135,12 @@ public class CachedPlayer } } - ApplyCharacterData(objectKind, moddedPaths); + ApplyBaseData(moddedPaths); + + foreach (var kind in objectKind) + { + ApplyCustomizationData(kind); + } }, downloadToken).ContinueWith(task => { if (!task.IsCanceled) return; @@ -144,7 +157,7 @@ public class CachedPlayer try { using var db = new FileCacheContext(); - foreach (var item in _cache.SelectMany(c => c.Value.FileReplacements).ToList()) + foreach (var item in _cachedData.FileReplacements.SelectMany(k => k.Value).ToList()) { foreach (var gamePath in item.GamePaths) { @@ -168,14 +181,16 @@ public class CachedPlayer return missingFiles; } - private unsafe void ApplyCharacterData(ObjectKind objectKind, Dictionary moddedPaths) + private void ApplyBaseData(Dictionary moddedPaths) { - if (PlayerCharacter is null) return; - var cache = _cache[objectKind]; - _ipcManager.PenumbraRemoveTemporaryCollection(PlayerName!); var tempCollection = _ipcManager.PenumbraCreateTemporaryCollection(PlayerName!); - _ipcManager.PenumbraSetTemporaryMods(tempCollection, moddedPaths, _cache.First().Value.ManipulationData); + _ipcManager.PenumbraSetTemporaryMods(tempCollection, moddedPaths, _cachedData.ManipulationData); + } + + private unsafe void ApplyCustomizationData(ObjectKind objectKind) + { + if (PlayerCharacter is null) return; if (objectKind == ObjectKind.Player) { @@ -183,7 +198,7 @@ public class CachedPlayer RequestedPenumbraRedraw = true; Logger.Debug( $"Request Redraw for {PlayerName}"); - _ipcManager.GlamourerApplyAll(cache.GlamourerData, PlayerCharacter!); + _ipcManager.GlamourerApplyAll(_cachedData.GlamourerData[objectKind], PlayerCharacter!); } else if (objectKind == ObjectKind.Minion) { @@ -191,7 +206,7 @@ public class CachedPlayer if (minion != null) { Logger.Debug($"Request Redraw for Minion"); - _ipcManager.GlamourerApplyAll(cache.GlamourerData, obj: (IntPtr)minion); + _ipcManager.GlamourerApplyAll(_cachedData.GlamourerData[objectKind], obj: (IntPtr)minion); } } else if (objectKind == ObjectKind.Pet) @@ -200,7 +215,7 @@ public class CachedPlayer if (pet != IntPtr.Zero) { Logger.Debug("Request Redraw for Pet"); - _ipcManager.GlamourerApplyAll(cache.GlamourerData, pet); + _ipcManager.GlamourerApplyAll(_cachedData.GlamourerData[objectKind], pet); } } else if (objectKind == ObjectKind.Companion) @@ -209,7 +224,7 @@ public class CachedPlayer if (companion != IntPtr.Zero) { Logger.Debug("Request Redraw for Companion"); - _ipcManager.GlamourerApplyAll(cache.GlamourerData, companion); + _ipcManager.GlamourerApplyAll(_cachedData.GlamourerData[objectKind], companion); } } else if (objectKind == ObjectKind.Mount) @@ -223,6 +238,49 @@ public class CachedPlayer } } + private unsafe void RevertCustomizationData(ObjectKind objectKind) + { + if (PlayerCharacter is null) return; + + if (objectKind == ObjectKind.Player) + { + _ipcManager.GlamourerApplyOnlyCustomization(_originalGlamourerData, PlayerCharacter); + _ipcManager.GlamourerApplyOnlyEquipment(_lastGlamourerData, PlayerCharacter); + } + else if (objectKind == ObjectKind.Minion) + { + var minion = ((Character*)PlayerCharacter.Address)->CompanionObject; + if (minion != null) + { + _ipcManager.PenumbraRedraw((IntPtr)minion); + } + } + else if (objectKind == ObjectKind.Pet) + { + var pet = _dalamudUtil.GetPet(PlayerCharacter.Address); + if (pet != IntPtr.Zero) + { + _ipcManager.PenumbraRedraw(pet); + } + } + else if (objectKind == ObjectKind.Companion) + { + var companion = _dalamudUtil.GetCompanion(PlayerCharacter.Address); + if (companion != IntPtr.Zero) + { + _ipcManager.PenumbraRedraw(companion); + } + } + else if (objectKind == ObjectKind.Mount) + { + var mount = ((CharaExt*)PlayerCharacter.Address)->Mount; + if (mount != null) + { + _ipcManager.PenumbraRedraw((IntPtr)mount); + } + } + } + public void DisposePlayer() { if (_isDisposed) return; @@ -234,16 +292,15 @@ public class CachedPlayer Logger.Verbose("Restoring state for " + PlayerName); _dalamudUtil.FrameworkUpdate -= DalamudUtilOnFrameworkUpdate; _ipcManager.PenumbraRedrawEvent -= IpcManagerOnPenumbraRedrawEvent; - foreach (var token in _downloadCancellationTokenSources) - { - token.Value?.Cancel(); - token.Value?.Dispose(); - } + _downloadCancellationTokenSource?.Cancel(); + _downloadCancellationTokenSource?.Dispose(); _ipcManager.PenumbraRemoveTemporaryCollection(PlayerName); if (PlayerCharacter != null && PlayerCharacter.IsValid()) { - _ipcManager.GlamourerApplyOnlyCustomization(_originalGlamourerData, PlayerCharacter); - _ipcManager.GlamourerApplyOnlyEquipment(_lastGlamourerData, PlayerCharacter); + foreach (var item in _cachedData.FileReplacements) + { + RevertCustomizationData(item.Key); + } } } catch (Exception ex) @@ -315,8 +372,7 @@ public class CachedPlayer if (RequestedPenumbraRedraw == false) { Logger.Warn("Unauthorized character change detected"); - // todo for everything I guess - DownloadAndApplyCharacter(ObjectKind.Player); + ApplyCustomizationData(ObjectKind.Player); } else { diff --git a/MareSynchronos/Managers/IpcManager.cs b/MareSynchronos/Managers/IpcManager.cs index b6ee87a..1f9cb1c 100644 --- a/MareSynchronos/Managers/IpcManager.cs +++ b/MareSynchronos/Managers/IpcManager.cs @@ -129,7 +129,6 @@ namespace MareSynchronos.Managers public void GlamourerApplyAll(string customization, GameObject character) { if (!CheckGlamourerApi()) return; - Logger.Verbose("Glamourer apply all to " + character); _glamourerApplyAll!.InvokeAction(customization, character); } @@ -150,7 +149,12 @@ namespace MareSynchronos.Managers public string GlamourerGetCharacterCustomization(GameObject character) { if (!CheckGlamourerApi()) return string.Empty; - return _glamourerGetAllCustomization!.InvokeFunc(character); + var glamourerString = _glamourerGetAllCustomization!.InvokeFunc(character); + byte[] bytes = Convert.FromBase64String(glamourerString); + // ignore transparency + bytes[88] = 128; + bytes[89] = 63; + return Convert.ToBase64String(bytes); } public void GlamourerRevertCharacterCustomization(GameObject character) @@ -206,7 +210,7 @@ namespace MareSynchronos.Managers { if (!CheckPenumbraApi()) return null; var resolvedPath = _penumbraResolvePlayer!.InvokeFunc(path); - Logger.Verbose("Resolved " + path + "=>" + string.Join(", ", resolvedPath)); + //Logger.Verbose("Resolved " + path + "=>" + string.Join(", ", resolvedPath)); return resolvedPath; } @@ -218,7 +222,7 @@ namespace MareSynchronos.Managers { resolvedPaths = new[] { path }; } - Logger.Verbose("Reverse Resolved " + path + "=>" + string.Join(", ", resolvedPaths)); + //Logger.Verbose("Reverse Resolved " + path + "=>" + string.Join(", ", resolvedPaths)); return resolvedPaths; } diff --git a/MareSynchronos/Managers/OnlinePlayerManager.cs b/MareSynchronos/Managers/OnlinePlayerManager.cs index 7fc8bd2..d20575b 100644 --- a/MareSynchronos/Managers/OnlinePlayerManager.cs +++ b/MareSynchronos/Managers/OnlinePlayerManager.cs @@ -71,9 +71,9 @@ public class OnlinePlayerManager : IDisposable } } - private void PlayerManagerOnPlayerHasChanged(CharacterCacheDto characterCache, ObjectKind objectKind) + private void PlayerManagerOnPlayerHasChanged(CharacterCacheDto characterCache) { - PushCharacterData(OnlineVisiblePlayerHashes, objectKind); + PushCharacterData(OnlineVisiblePlayerHashes); } private void ApiControllerOnConnected() @@ -173,7 +173,7 @@ public class OnlinePlayerManager : IDisposable { if (_onlineCachedPlayers.Any(p => p.PlayerNameHash == characterNameHash)) { - PushCharacterData(new List() { characterNameHash }, ObjectKind.Player); + PushCharacterData(new List() { characterNameHash }); _playerTokenDisposal.TryGetValue(_onlineCachedPlayers.Single(p => p.PlayerNameHash == characterNameHash), out var cancellationTokenSource); cancellationTokenSource?.Cancel(); return; @@ -242,25 +242,19 @@ public class OnlinePlayerManager : IDisposable if (newlyVisiblePlayers.Any()) { Logger.Verbose("Has new visible players, pushing character data"); - PushCharacterData(newlyVisiblePlayers, ObjectKind.Player); - PushCharacterData(newlyVisiblePlayers, ObjectKind.Pet); - PushCharacterData(newlyVisiblePlayers, ObjectKind.Minion); - PushCharacterData(newlyVisiblePlayers, ObjectKind.Companion); - PushCharacterData(newlyVisiblePlayers, ObjectKind.Mount); + PushCharacterData(newlyVisiblePlayers); } _lastPlayerObjectCheck = DateTime.Now; } - private void PushCharacterData(List visiblePlayers, ObjectKind objectKind) + private void PushCharacterData(List visiblePlayers) { - _playerManager.LastSentCharacterData.TryGetValue(objectKind, out var characterData); - if (visiblePlayers.Any() && characterData != null) + if (visiblePlayers.Any() && _playerManager.LastCreatedCharacterData != null) { Task.Run(async () => { - Logger.Verbose(JsonConvert.SerializeObject(_playerManager.LastSentCharacterData[objectKind], Formatting.Indented)); - await _apiController.PushCharacterData(_playerManager.LastSentCharacterData[objectKind]!, + await _apiController.PushCharacterData(_playerManager.LastCreatedCharacterData!, visiblePlayers); }); } diff --git a/MareSynchronos/Managers/PlayerManager.cs b/MareSynchronos/Managers/PlayerManager.cs index 6e9726c..01b7b16 100644 --- a/MareSynchronos/Managers/PlayerManager.cs +++ b/MareSynchronos/Managers/PlayerManager.cs @@ -1,15 +1,12 @@ using MareSynchronos.Factories; using MareSynchronos.Utils; using MareSynchronos.WebAPI; -using Newtonsoft.Json; using System; using System.Threading; using System.Threading.Tasks; using MareSynchronos.API; using Penumbra.GameData.Structs; -using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.Game.Character; -using Penumbra.GameData.ByteString; using System.Collections.Generic; using System.Linq; using MareSynchronos.Models; @@ -17,7 +14,7 @@ using MareSynchronos.Interop; namespace MareSynchronos.Managers { - public delegate void PlayerHasChanged(CharacterCacheDto characterCache, ObjectKind objectKind); + public delegate void PlayerHasChanged(CharacterCacheDto characterCache); public class PlayerManager : IDisposable { @@ -26,17 +23,15 @@ namespace MareSynchronos.Managers private readonly DalamudUtil _dalamudUtil; private readonly IpcManager _ipcManager; public event PlayerHasChanged? PlayerHasChanged; - public bool SendingData { get; private set; } - public Dictionary LastSentCharacterData { get; private set; } = new(); + public CharacterCacheDto? LastCreatedCharacterData { get; private set; } + public CharacterData PermanentDataCache { get; private set; } = new(); + private readonly Dictionary> objectKindsToUpdate = new(); - private Dictionary _playerChangedCts = new(); + private CancellationTokenSource? _playerChangedCts = new(); private DateTime _lastPlayerObjectCheck; - private Dictionary _currentCharacterEquipment = new(); + private CharacterEquipment? _currentCharacterEquipment = new(); - private PlayerAttachedObject Minion; - private PlayerAttachedObject Pet; - private PlayerAttachedObject Companion; - private PlayerAttachedObject Mount; + private List playerAttachedObjects = new List(); public unsafe PlayerManager(ApiController apiController, IpcManager ipcManager, CharacterDataFactory characterDataFactory, DalamudUtil dalamudUtil) @@ -50,6 +45,7 @@ namespace MareSynchronos.Managers _apiController.Connected += ApiControllerOnConnected; _apiController.Disconnected += ApiController_Disconnected; + _dalamudUtil.FrameworkUpdate += DalamudUtilOnFrameworkUpdate; Logger.Debug("Watching Player, ApiController is Connected: " + _apiController.IsConnected); if (_apiController.IsConnected) @@ -57,10 +53,14 @@ namespace MareSynchronos.Managers ApiControllerOnConnected(); } - Minion = new PlayerAttachedObject(ObjectKind.Minion, IntPtr.Zero, IntPtr.Zero, () => (IntPtr)((Character*)_dalamudUtil.PlayerPointer)->CompanionObject); - Pet = new PlayerAttachedObject(ObjectKind.Pet, IntPtr.Zero, IntPtr.Zero, () => _dalamudUtil.GetPet()); - Companion = new PlayerAttachedObject(ObjectKind.Companion, IntPtr.Zero, IntPtr.Zero, () => _dalamudUtil.GetCompanion()); - Mount = new PlayerAttachedObject(ObjectKind.Mount, IntPtr.Zero, IntPtr.Zero, () => (IntPtr)((CharaExt*)_dalamudUtil.PlayerPointer)->Mount); + playerAttachedObjects = new List() + { + new PlayerOrRelatedObject(ObjectKind.Player, IntPtr.Zero, IntPtr.Zero, () => _dalamudUtil.PlayerPointer), + new PlayerOrRelatedObject(ObjectKind.Minion, IntPtr.Zero, IntPtr.Zero, () => (IntPtr)((Character*)_dalamudUtil.PlayerPointer)->CompanionObject), + new PlayerOrRelatedObject(ObjectKind.Pet, IntPtr.Zero, IntPtr.Zero, () => _dalamudUtil.GetPet()), + new PlayerOrRelatedObject(ObjectKind.Companion, IntPtr.Zero, IntPtr.Zero, () => _dalamudUtil.GetCompanion()), + new PlayerOrRelatedObject(ObjectKind.Mount, IntPtr.Zero, IntPtr.Zero, () => (IntPtr)((CharaExt*)_dalamudUtil.PlayerPointer)->Mount), + }; } public void Dispose() @@ -74,49 +74,18 @@ namespace MareSynchronos.Managers _dalamudUtil.FrameworkUpdate -= DalamudUtilOnFrameworkUpdate; } - private unsafe void CheckAndUpdateObject(PlayerAttachedObject attachedObject) - { - var curPtr = attachedObject.CurrentAddress; - if (curPtr != IntPtr.Zero) - { - var chara = (Character*)curPtr; - if (attachedObject.Address == IntPtr.Zero || attachedObject.Address != curPtr - || attachedObject.CompareAndUpdateEquipment(chara->EquipSlotData, chara->CustomizeData) - || (chara->GameObject.DrawObject != null && (IntPtr)chara->GameObject.DrawObject != attachedObject.DrawObjectAddress)) - { - Logger.Verbose(attachedObject.ObjectKind + " Changed " + curPtr); - - attachedObject.Address = curPtr; - attachedObject.DrawObjectAddress = (IntPtr)chara->GameObject.DrawObject; - attachedObject.CompareAndUpdateEquipment(chara->EquipSlotData, chara->CustomizeData); - OnPlayerChanged(attachedObject.ObjectKind); - } - } - else - { - attachedObject.Address = IntPtr.Zero; - LastSentCharacterData[attachedObject.ObjectKind] = null; - } - } - private unsafe void DalamudUtilOnFrameworkUpdate() { - if (!_dalamudUtil.IsPlayerPresent || !_ipcManager.Initialized || !_apiController.IsConnected) return; - //_dalamudUtil.DebugPrintRenderFlags(_dalamudUtil.PlayerPointer); + if (!_dalamudUtil.IsPlayerPresent || !_ipcManager.Initialized) return; if (DateTime.Now < _lastPlayerObjectCheck.AddSeconds(0.25)) return; - - if (_dalamudUtil.IsPlayerPresent && !_currentCharacterEquipment[ObjectKind.Player]!.CompareAndUpdate(_dalamudUtil.PlayerCharacter)) + playerAttachedObjects.ForEach(k => k.CheckAndUpdateObject()); + if (playerAttachedObjects.Any(c => c.HasUnprocessedUpdate && !c.IsProcessing)) { - OnPlayerChanged(ObjectKind.Player); + OnPlayerOrAttachedObjectsChanged(); } - CheckAndUpdateObject(Minion); - CheckAndUpdateObject(Pet); - CheckAndUpdateObject(Companion); - CheckAndUpdateObject(Mount); - _lastPlayerObjectCheck = DateTime.Now; } @@ -125,10 +94,6 @@ namespace MareSynchronos.Managers Logger.Debug("ApiController Connected"); _ipcManager.PenumbraRedrawEvent += IpcManager_PenumbraRedrawEvent; - _dalamudUtil.FrameworkUpdate += DalamudUtilOnFrameworkUpdate; - - _currentCharacterEquipment[ObjectKind.Player] = new CharacterEquipment(_dalamudUtil.PlayerCharacter); - PlayerChanged(ObjectKind.Player); } private void ApiController_Disconnected() @@ -136,82 +101,63 @@ namespace MareSynchronos.Managers Logger.Debug(nameof(ApiController_Disconnected)); _ipcManager.PenumbraRedrawEvent -= IpcManager_PenumbraRedrawEvent; - _dalamudUtil.FrameworkUpdate -= DalamudUtilOnFrameworkUpdate; - LastSentCharacterData.Clear(); } - private async Task CreateFullCharacterCache(CancellationToken token, ObjectKind objectKind) + private async Task CreateFullCharacterCacheDto(CancellationToken token) { - IntPtr pointer = objectKind switch + foreach (var unprocessedObject in playerAttachedObjects.Where(c => c.HasUnprocessedUpdate).ToList()) { - ObjectKind.Player => _dalamudUtil.PlayerPointer, - ObjectKind.Minion => Minion.Address, - ObjectKind.Pet => Pet.Address, - ObjectKind.Companion => Companion.Address, - ObjectKind.Mount => Mount.Address, - _ => throw new NotImplementedException() - }; + Logger.Verbose("Building Cache for " + unprocessedObject.ObjectKind); + PermanentDataCache = _characterDataFactory.BuildCharacterData(PermanentDataCache, unprocessedObject.ObjectKind, unprocessedObject.Address); + unprocessedObject.HasUnprocessedUpdate = false; + unprocessedObject.IsProcessing = false; + token.ThrowIfCancellationRequested(); + } - var cache = _characterDataFactory.BuildCharacterData(pointer); - if (cache == null) return null; - CharacterCacheDto? cacheDto = null; - - await Task.Run(async () => + while (!PermanentDataCache.IsReady && !token.IsCancellationRequested) { - while (!cache.IsReady && !token.IsCancellationRequested) - { - await Task.Delay(50, token); - } + await Task.Delay(50, token); + } - if (token.IsCancellationRequested) return; - cache.Kind = objectKind; - cacheDto = cache.ToCharacterCacheDto(); - var json = JsonConvert.SerializeObject(cacheDto); + if (token.IsCancellationRequested) return null; - cacheDto.Hash = Crypto.GetHash(json); - }, token); + Logger.Verbose("Cache creation complete"); - return cacheDto; + return PermanentDataCache.ToCharacterCacheDto(); } private void IpcManager_PenumbraRedrawEvent(IntPtr address, int idx) { Logger.Verbose("RedrawEvent for addr " + address); - if (address == _dalamudUtil.PlayerPointer) + foreach (var item in playerAttachedObjects) { - Logger.Debug("Penumbra Redraw Event for " + _dalamudUtil.PlayerName); - PlayerChanged(ObjectKind.Player); + if (address == item.Address) + { + Logger.Debug("Penumbra redraw Event for " + item.ObjectKind); + item.HasUnprocessedUpdate = true; + } } - if (address == Minion.Address) + if (playerAttachedObjects.Any(c => c.HasUnprocessedUpdate)) { - Logger.Debug("Penumbra Redraw Event for Minion"); - PlayerChanged(ObjectKind.Minion); - } - - if (address == Pet.Address) - { - Logger.Debug("Penumbra Redraw Event for Pet"); - PlayerChanged(ObjectKind.Pet); - } - - if (address == Companion.Address) - { - Logger.Debug("Penumbra Redraw Event for Companion"); - PlayerChanged(ObjectKind.Companion); + OnPlayerOrAttachedObjectsChanged(); } } - private void PlayerChanged(ObjectKind objectKind) + private void OnPlayerOrAttachedObjectsChanged() { if (_dalamudUtil.IsInGpose) return; - Logger.Debug("Object changed: " + objectKind.ToString()); - _playerChangedCts.TryGetValue(objectKind, out var cts); - cts?.Cancel(); - _playerChangedCts[objectKind] = new CancellationTokenSource(); - var token = _playerChangedCts[objectKind]!.Token; + var unprocessedObjects = playerAttachedObjects.Where(c => c.HasUnprocessedUpdate); + foreach (var unprocessedObject in unprocessedObjects) + { + unprocessedObject.IsProcessing = true; + } + Logger.Debug("Object(s) changed: " + string.Join(", ", unprocessedObjects.Select(c => c.ObjectKind))); + _playerChangedCts?.Cancel(); + _playerChangedCts = new CancellationTokenSource(); + var token = _playerChangedCts.Token; // fix for redraw from anamnesis while ((!_dalamudUtil.IsPlayerPresent || _dalamudUtil.PlayerName == "--") && !token.IsCancellationRequested) @@ -234,45 +180,27 @@ namespace MareSynchronos.Managers Task.Run(async () => { - SendingData = true; - int attempts = 0; - while (!_apiController.IsConnected && attempts < 10 && !token.IsCancellationRequested) - { - Logger.Warn("No connection to the API"); - await Task.Delay(TimeSpan.FromSeconds(1), token); - attempts++; - } - - if (attempts == 10 || token.IsCancellationRequested) return; - _dalamudUtil.WaitWhileSelfIsDrawing(token); - var characterCache = (await CreateFullCharacterCache(token, objectKind)); + CharacterCacheDto? cacheDto = (await CreateFullCharacterCacheDto(token)); + if (cacheDto == null || token.IsCancellationRequested) return; - if (characterCache == null || token.IsCancellationRequested) return; - - LastSentCharacterData.TryGetValue(objectKind, out var lastSentPlayerData); - if (characterCache.Hash == (lastSentPlayerData?.Hash ?? string.Empty)) + if ((LastCreatedCharacterData?.GetHashCode() ?? 0) == cacheDto.GetHashCode()) { Logger.Debug("Not sending data, already sent"); return; } + else + { + LastCreatedCharacterData = cacheDto; + } - LastSentCharacterData[objectKind] = characterCache; - PlayerHasChanged?.Invoke(characterCache, objectKind); - SendingData = false; + if (_apiController.IsConnected) + { + Logger.Verbose("Invoking PlayerHasChanged"); + PlayerHasChanged?.Invoke(cacheDto); + } }, token); } - - private void OnPlayerChanged(ObjectKind objectKind) - { - Task.Run(() => - { - Logger.Debug("Watcher: PlayerChanged"); - PlayerChanged(objectKind); - }); - } - - } } diff --git a/MareSynchronos/Models/CharacterData.cs b/MareSynchronos/Models/CharacterData.cs index e683452..8075aac 100644 --- a/MareSynchronos/Models/CharacterData.cs +++ b/MareSynchronos/Models/CharacterData.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Text; using MareSynchronos.API; -using MareSynchronos.Factories; namespace MareSynchronos.Models { @@ -11,28 +10,30 @@ namespace MareSynchronos.Models public class CharacterData { [JsonProperty] - public ObjectKind Kind { get; set; } - public List FileReplacements { get; set; } = new(); + public Dictionary> FileReplacements { get; set; } = new(); [JsonProperty] - public string GlamourerString { get; set; } = string.Empty; + public Dictionary GlamourerString { get; set; } = new(); - public bool IsReady => FileReplacements.All(f => f.Computed); + public bool IsReady => FileReplacements.SelectMany(k => k.Value).All(f => f.Computed); + [JsonProperty] public string ManipulationString { get; set; } = string.Empty; - public void AddFileReplacement(FileReplacement fileReplacement) + public void AddFileReplacement(ObjectKind objectKind, FileReplacement fileReplacement) { if (!fileReplacement.HasFileReplacement) return; - var existingReplacement = FileReplacements.SingleOrDefault(f => f.ResolvedPath == fileReplacement.ResolvedPath); + if (!FileReplacements.ContainsKey(objectKind)) FileReplacements.Add(objectKind, new List()); + + var existingReplacement = FileReplacements[objectKind].SingleOrDefault(f => f.ResolvedPath == fileReplacement.ResolvedPath); if (existingReplacement != null) { existingReplacement.GamePaths.AddRange(fileReplacement.GamePaths.Where(e => !existingReplacement.GamePaths.Contains(e))); } else { - FileReplacements.Add(fileReplacement); + FileReplacements[objectKind].Add(fileReplacement); } } @@ -40,15 +41,14 @@ namespace MareSynchronos.Models { return new CharacterCacheDto() { - ObjectKind = Kind, - FileReplacements = FileReplacements.Where(f => f.HasFileReplacement).GroupBy(f => f.Hash).Select(g => + FileReplacements = FileReplacements.ToDictionary(k => k.Key, k => k.Value.Where(f => f.HasFileReplacement).GroupBy(f => f.Hash).Select(g => { return new FileReplacementDto() { GamePaths = g.SelectMany(g => g.GamePaths).Distinct().ToArray(), Hash = g.First().Hash }; - }).ToList(), + }).ToList()), GlamourerData = GlamourerString, ManipulationData = ManipulationString }; @@ -57,7 +57,7 @@ namespace MareSynchronos.Models public override string ToString() { StringBuilder stringBuilder = new(); - foreach (var fileReplacement in FileReplacements.OrderBy(a => a.GamePaths[0])) + foreach (var fileReplacement in FileReplacements.SelectMany(k => k.Value).OrderBy(a => a.GamePaths[0])) { stringBuilder.AppendLine(fileReplacement.ToString()); } diff --git a/MareSynchronos/Models/PlayerAttachedObject.cs b/MareSynchronos/Models/PlayerAttachedObject.cs index 130b1fb..87d06e5 100644 --- a/MareSynchronos/Models/PlayerAttachedObject.cs +++ b/MareSynchronos/Models/PlayerAttachedObject.cs @@ -2,33 +2,76 @@ using MareSynchronos.API; using FFXIVClientStructs.FFXIV.Client.Game.Character; using System.Runtime.InteropServices; +using MareSynchronos.Utils; +using Penumbra.GameData.ByteString; namespace MareSynchronos.Models { - internal class PlayerAttachedObject + internal class PlayerOrRelatedObject { private readonly Func getAddress; public unsafe Character* Character => (Character*)Address; + private string _name; + public ObjectKind ObjectKind { get; } public IntPtr Address { get; set; } public IntPtr DrawObjectAddress { get; set; } - public IntPtr CurrentAddress => getAddress.Invoke(); + private IntPtr CurrentAddress => getAddress.Invoke(); - public PlayerAttachedObject(ObjectKind objectKind, IntPtr address, IntPtr drawObjectAddress, Func getAddress) + public PlayerOrRelatedObject(ObjectKind objectKind, IntPtr address, IntPtr drawObjectAddress, Func getAddress) { ObjectKind = objectKind; Address = address; DrawObjectAddress = drawObjectAddress; this.getAddress = getAddress; + _name = string.Empty; } public byte[] EquipSlotData { get; set; } = new byte[40]; public byte[] CustomizeData { get; set; } = new byte[26]; - public unsafe bool CompareAndUpdateEquipment(byte* equipSlotData, byte* customizeData) + public bool HasUnprocessedUpdate { get; set; } = false; + public bool IsProcessing { get; set; } = false; + + public unsafe void CheckAndUpdateObject() + { + var curPtr = CurrentAddress; + if (curPtr != IntPtr.Zero) + { + var chara = (Character*)curPtr; + bool addr = Address == IntPtr.Zero || Address != curPtr; + bool equip = CompareAndUpdateByteData(chara->EquipSlotData, chara->CustomizeData); + bool drawObj = (chara->GameObject.DrawObject != null && (IntPtr)chara->GameObject.DrawObject != DrawObjectAddress); + var name = new Utf8String(chara->GameObject.Name).ToString(); + bool nameChange = (name != _name); + if (addr || equip || drawObj || nameChange) + { + _name = name; + Logger.Verbose(ObjectKind + " Changed: " + _name + ", now: " + curPtr + ", " + (IntPtr)chara->GameObject.DrawObject); + + Address = curPtr; + DrawObjectAddress = (IntPtr)chara->GameObject.DrawObject; + HasUnprocessedUpdate = true; + } + } + else + { + if (Address != IntPtr.Zero || DrawObjectAddress != IntPtr.Zero) + { + Address = IntPtr.Zero; + DrawObjectAddress = IntPtr.Zero; + HasUnprocessedUpdate = true; + } + + Address = IntPtr.Zero; + DrawObjectAddress = IntPtr.Zero; + } + } + + private unsafe bool CompareAndUpdateByteData(byte* equipSlotData, byte* customizeData) { bool hasChanges = false; for (int i = 0; i < EquipSlotData.Length; i++) diff --git a/MareSynchronos/WebAPI/ApIController.Functions.Files.cs b/MareSynchronos/WebAPI/ApIController.Functions.Files.cs index bdce71c..110de12 100644 --- a/MareSynchronos/WebAPI/ApIController.Functions.Files.cs +++ b/MareSynchronos/WebAPI/ApIController.Functions.Files.cs @@ -17,13 +17,12 @@ namespace MareSynchronos.WebAPI public partial class ApiController { private int _downloadId = 0; - public void CancelUpload(ObjectKind objectKind) + public void CancelUpload() { - _uploadTokens.TryGetValue(objectKind, out var cts); - if (cts != null) + if (_uploadToken != null) { - Logger.Warn("Cancelling upload"); - cts?.Cancel(); + Logger.Debug("Cancelling upload"); + _uploadToken?.Cancel(); _mareHub!.SendAsync(Api.SendFileAbortUpload); CurrentUploads.Clear(); } @@ -41,7 +40,6 @@ namespace MareSynchronos.WebAPI await using var fs = File.OpenWrite(fileName); await foreach (var data in reader.WithCancellation(ct)) { - //Logger.Debug("Getting chunk of " + hash); CurrentDownloads[downloadId].Single(f => f.Hash == hash).Transferred += data.Length; await fs.WriteAsync(data, ct); } @@ -124,12 +122,12 @@ namespace MareSynchronos.WebAPI if (!IsConnected || SecretKey == "-") return; Logger.Debug("Sending Character data to service " + ApiUri); - CancelUpload(character.ObjectKind); - _uploadTokens[character.ObjectKind] = new CancellationTokenSource(); - var uploadToken = _uploadTokens[character.ObjectKind]!.Token; + CancelUpload(); + _uploadToken = new CancellationTokenSource(); + var uploadToken = _uploadToken.Token; Logger.Verbose("New Token Created"); - var filesToUpload = await _mareHub!.InvokeAsync>(Api.InvokeFileSendFiles, character.FileReplacements.Select(c => c.Hash).Distinct(), uploadToken); + var filesToUpload = await _mareHub!.InvokeAsync>(Api.InvokeFileSendFiles, character.FileReplacements.SelectMany(c => c.Value.Select(v => v.Hash)).Distinct(), uploadToken); foreach (var file in filesToUpload.Where(f => !f.IsForbidden)) { @@ -205,8 +203,8 @@ namespace MareSynchronos.WebAPI Logger.Warn("=== Upload operation was cancelled ==="); } - Logger.Verbose("Upload complete for " + character.Hash); - _uploadTokens.Remove(character.ObjectKind); + Logger.Verbose("Upload complete for " + character.GetHashCode()); + _uploadToken = null; } private async Task<(string, byte[])> GetCompressedFileData(string fileHash, CancellationToken uploadToken) diff --git a/MareSynchronos/WebAPI/ApiController.Connectivity.cs b/MareSynchronos/WebAPI/ApiController.Connectivity.cs index 951c3f0..7130caf 100644 --- a/MareSynchronos/WebAPI/ApiController.Connectivity.cs +++ b/MareSynchronos/WebAPI/ApiController.Connectivity.cs @@ -37,7 +37,7 @@ namespace MareSynchronos.WebAPI private HubConnection? _mareHub; - private Dictionary _uploadTokens = new(); + private CancellationTokenSource? _uploadToken = new(); private ConnectionDto? _connectionDto; public SystemInfoDto SystemInfoDto { get; private set; } = new(); @@ -268,10 +268,7 @@ namespace MareSynchronos.WebAPI { CurrentUploads.Clear(); CurrentDownloads.Clear(); - foreach(var token in _uploadTokens.Values) - { - token?.Cancel(); - } + _uploadToken?.Cancel(); Logger.Debug("Connection closed"); Disconnected?.Invoke(); return Task.CompletedTask; @@ -289,10 +286,7 @@ namespace MareSynchronos.WebAPI { CurrentUploads.Clear(); CurrentDownloads.Clear(); - foreach (var token in _uploadTokens.Values) - { - token?.Cancel(); - } + _uploadToken?.Cancel(); Logger.Debug("Connection closed... Reconnecting"); Disconnected?.Invoke(); return Task.CompletedTask;