diff --git a/MareSynchronos/Factories/CharacterDataFactory.cs b/MareSynchronos/Factories/CharacterDataFactory.cs index fd3de0a..1c5d312 100644 --- a/MareSynchronos/Factories/CharacterDataFactory.cs +++ b/MareSynchronos/Factories/CharacterDataFactory.cs @@ -3,6 +3,8 @@ 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; @@ -22,16 +24,23 @@ public class CharacterDataFactory { private readonly DalamudUtil _dalamudUtil; private readonly IpcManager _ipcManager; + private readonly TransientResourceManager transientResourceManager; - public CharacterDataFactory(DalamudUtil dalamudUtil, IpcManager ipcManager) + public CharacterDataFactory(DalamudUtil dalamudUtil, IpcManager ipcManager, TransientResourceManager transientResourceManager) { Logger.Verbose("Creating " + nameof(CharacterDataFactory)); _dalamudUtil = dalamudUtil; _ipcManager = ipcManager; + this.transientResourceManager = transientResourceManager; } - public unsafe CharacterData BuildCharacterData(CharacterData previousData, ObjectKind objectKind, IntPtr playerPointer, CancellationToken token) + private unsafe bool CheckForPointer(IntPtr playerPointer) + { + return playerPointer == IntPtr.Zero || ((Character*)playerPointer)->GameObject.GetDrawObject() == null; + } + + public CharacterData BuildCharacterData(CharacterData previousData, ObjectKind objectKind, IntPtr playerPointer, CancellationToken token) { if (!_ipcManager.Initialized) { @@ -41,7 +50,7 @@ public class CharacterDataFactory bool pointerIsZero = true; try { - pointerIsZero = playerPointer == IntPtr.Zero || ((Character*)playerPointer)->GameObject.GetDrawObject() == null; + pointerIsZero = CheckForPointer(playerPointer); } catch (Exception ex) { @@ -144,7 +153,6 @@ public class CharacterDataFactory return; } - //Logger.Verbose("Adding File Replacement for Material " + fileName); var mtrlPath = fileName.Split("|")[2]; if (cache.FileReplacements.ContainsKey(objectKind)) @@ -179,12 +187,28 @@ public class CharacterDataFactory } } + private void AddReplacement(string varPath, ObjectKind objectKind, CharacterData cache, int inheritanceLevel = 0, bool doNotReverseResolve = false) + { + if (varPath.IsNullOrEmpty()) return; + + if (cache.FileReplacements.ContainsKey(objectKind)) + { + if (cache.FileReplacements[objectKind].Any(c => c.GamePaths.Contains(varPath))) + { + return; + } + } + + var variousReplacement = CreateFileReplacement(varPath, doNotReverseResolve); + DebugPrint(variousReplacement, objectKind, "Various", inheritanceLevel); + + cache.AddFileReplacement(objectKind, variousReplacement); + } + private void AddReplacementsFromTexture(string texPath, ObjectKind objectKind, CharacterData cache, int inheritanceLevel = 0, bool doNotReverseResolve = true) { if (string.IsNullOrEmpty(texPath)) return; - //Logger.Verbose("Adding File Replacement for Texture " + texPath); - if (cache.FileReplacements.ContainsKey(objectKind)) { if (cache.FileReplacements[objectKind].Any(c => c.GamePaths.Contains(texPath))) @@ -215,14 +239,16 @@ public class CharacterDataFactory previousData.FileReplacements[objectKind].Clear(); } - Stopwatch st = Stopwatch.StartNew(); 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(charaPointer); + + _dalamudUtil.WaitWhileCharacterIsDrawing(objectKind.ToString(), charaPointer); + + Stopwatch st = Stopwatch.StartNew(); previousData.ManipulationString = _ipcManager.PenumbraGetMetaManipulations(); @@ -242,57 +268,131 @@ public class CharacterDataFactory AddReplacementsFromRenderModel(mdl, objectKind, previousData, 0); } + foreach (var item in previousData.FileReplacements[objectKind]) + { + transientResourceManager.RemoveTransientResource(charaPointer, item); + } + if (objectKind == ObjectKind.Player) { - var weaponObject = (Weapon*)((Object*)human)->ChildObject; - - if ((IntPtr)weaponObject != IntPtr.Zero) - { - var mainHandWeapon = weaponObject->WeaponRenderModel->RenderModel; - - AddReplacementsFromRenderModel(mainHandWeapon, objectKind, previousData, 0); - - if (weaponObject->NextSibling != (IntPtr)weaponObject) - { - var offHandWeapon = ((Weapon*)weaponObject->NextSibling)->WeaponRenderModel->RenderModel; - - AddReplacementsFromRenderModel(offHandWeapon, objectKind, previousData, 1); - } - } - - AddReplacementSkeleton(((HumanExt*)human)->Human.RaceSexId, objectKind, previousData); - try - { - AddReplacementsFromTexture(new Utf8String(((HumanExt*)human)->Decal->FileName()).ToString(), objectKind, previousData, 0, false); - } - catch - { - Logger.Warn("Could not get Decal data"); - } - try - { - AddReplacementsFromTexture(new Utf8String(((HumanExt*)human)->LegacyBodyDecal->FileName()).ToString(), objectKind, previousData, 0, false); - } - catch - { - Logger.Warn("Could not get Legacy Body Decal Data"); - } + AddPlayerSpecificReplacements(previousData, objectKind, charaPointer, human); } + if (objectKind == ObjectKind.Pet) + { + foreach (var item in previousData.FileReplacements[objectKind]) + { + transientResourceManager.AddSemiTransientResource(objectKind, item); + } + + previousData.FileReplacements[objectKind].Clear(); + } + + ManageSemiTransientData(previousData, objectKind, charaPointer); + st.Stop(); Logger.Verbose("Building " + objectKind + " Data took " + st.Elapsed); - return previousData; } + private unsafe void ManageSemiTransientData(CharacterData previousData, ObjectKind objectKind, IntPtr charaPointer) + { + transientResourceManager.PersistTransientResources(charaPointer, objectKind, CreateFileReplacement); + + foreach (var item in transientResourceManager.GetSemiTransientResources(objectKind)) + { + if (!previousData.FileReplacements.ContainsKey(objectKind)) + { + previousData.FileReplacements.Add(objectKind, new()); + } + + if (!previousData.FileReplacements[objectKind].Any(k => k.ResolvedPath.ToLowerInvariant() == item.ResolvedPath.ToLowerInvariant())) + { + if (_ipcManager.PenumbraResolvePath(item.GamePaths.First()).ToLowerInvariant() == item.GamePaths.First().ToLowerInvariant()) + { + transientResourceManager.RemoveTransientResource(charaPointer, item); + } + else + { + Logger.Verbose("Found semi transient resource: " + item); + previousData.FileReplacements[objectKind].Add(item); + } + } + } + } + + private unsafe void AddPlayerSpecificReplacements(CharacterData previousData, ObjectKind objectKind, IntPtr charaPointer, Human* human) + { + var weaponObject = (Weapon*)((Object*)human)->ChildObject; + + if ((IntPtr)weaponObject != IntPtr.Zero) + { + var mainHandWeapon = weaponObject->WeaponRenderModel->RenderModel; + + AddReplacementsFromRenderModel(mainHandWeapon, objectKind, previousData, 0); + + foreach (var item in previousData.FileReplacements[objectKind]) + { + transientResourceManager.RemoveTransientResource(charaPointer, item); + } + + foreach (var item in transientResourceManager.GetTransientResources((IntPtr)weaponObject)) + { + Logger.Verbose("Found transient weapon resource: " + item); + AddReplacement(item, objectKind, previousData, 1, true); + } + + if (weaponObject->NextSibling != (IntPtr)weaponObject) + { + var offHandWeapon = ((Weapon*)weaponObject->NextSibling)->WeaponRenderModel->RenderModel; + + AddReplacementsFromRenderModel(offHandWeapon, objectKind, previousData, 1); + + foreach (var item in previousData.FileReplacements[objectKind]) + { + transientResourceManager.RemoveTransientResource((IntPtr)offHandWeapon, item); + } + + foreach (var item in transientResourceManager.GetTransientResources((IntPtr)offHandWeapon)) + { + Logger.Verbose("Found transient offhand weapon resource: " + item); + AddReplacement(item, objectKind, previousData, 1, true); + } + } + } + + AddReplacementSkeleton(((HumanExt*)human)->Human.RaceSexId, objectKind, previousData); + try + { + AddReplacementsFromTexture(new Utf8String(((HumanExt*)human)->Decal->FileName()).ToString(), objectKind, previousData, 0, false); + } + catch + { + Logger.Warn("Could not get Decal data"); + } + try + { + AddReplacementsFromTexture(new Utf8String(((HumanExt*)human)->LegacyBodyDecal->FileName()).ToString(), objectKind, previousData, 0, false); + } + catch + { + Logger.Warn("Could not get Legacy Body Decal Data"); + } + + foreach (var item in previousData.FileReplacements[objectKind]) + { + transientResourceManager.RemoveTransientResource(charaPointer, item); + } + + previousData.HeelsOffset = _ipcManager.GetHeelsOffset(); + } + 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); - var replacement = CreateFileReplacement(skeletonPath, true); cache.AddFileReplacement(objectKind, replacement); diff --git a/MareSynchronos/Managers/CachedPlayer.cs b/MareSynchronos/Managers/CachedPlayer.cs index 2380168..5c6ebd9 100644 --- a/MareSynchronos/Managers/CachedPlayer.cs +++ b/MareSynchronos/Managers/CachedPlayer.cs @@ -12,6 +12,7 @@ using MareSynchronos.Interop; using MareSynchronos.Models; using MareSynchronos.Utils; using MareSynchronos.WebAPI; +using Newtonsoft.Json; namespace MareSynchronos.Managers; @@ -40,7 +41,7 @@ public class CachedPlayer } } - private bool _isDisposed = false; + private bool _isDisposed = true; private CancellationTokenSource? _downloadCancellationTokenSource = new(); private string _lastGlamourerData = string.Empty; @@ -142,6 +143,7 @@ public class CachedPlayer { Dictionary moddedPaths; int attempts = 0; + //Logger.Verbose(JsonConvert.SerializeObject(_cachedData, Formatting.Indented)); while ((toDownloadReplacements = TryCalculateModdedDictionary(out moddedPaths)).Count > 0 && attempts++ <= 10) { Logger.Debug("Downloading missing files for player " + PlayerName + ", kind: " + objectKind); @@ -162,19 +164,9 @@ public class CachedPlayer ApplyBaseData(moddedPaths); } - if (_dalamudUtil.IsInGpose) - { - Logger.Verbose("Player is in GPose, waiting"); - while (_dalamudUtil.IsInGpose) - { - await Task.Delay(TimeSpan.FromSeconds(0.5)); - downloadToken.ThrowIfCancellationRequested(); - } - } - foreach (var kind in objectKind) { - ApplyCustomizationData(kind); + ApplyCustomizationData(kind, downloadToken); } }, downloadToken).ContinueWith(task => { @@ -194,7 +186,7 @@ public class CachedPlayer try { using var db = new FileCacheContext(); - foreach (var item in _cachedData.FileReplacements.SelectMany(k => k.Value).ToList()) + foreach (var item in _cachedData.FileReplacements.SelectMany(k => k.Value.Where(v => string.IsNullOrEmpty(v.FileSwapPath))).ToList()) { foreach (var gamePath in item.GamePaths) { @@ -205,10 +197,20 @@ public class CachedPlayer } else { + Logger.Verbose("Missing file: " + item.Hash); missingFiles.Add(item); } } } + + foreach (var item in _cachedData.FileReplacements.SelectMany(k => k.Value.Where(v => !string.IsNullOrEmpty(v.FileSwapPath))).ToList()) + { + foreach (var gamePath in item.GamePaths) + { + Logger.Verbose("Adding file swap for " + gamePath + ":" + item.FileSwapPath); + moddedDictionary[gamePath] = item.FileSwapPath; + } + } } catch (Exception ex) { @@ -224,14 +226,16 @@ public class CachedPlayer _ipcManager.PenumbraSetTemporaryMods(PlayerName!, moddedPaths, _cachedData.ManipulationData); } - private unsafe void ApplyCustomizationData(ObjectKind objectKind) + private unsafe void ApplyCustomizationData(ObjectKind objectKind, CancellationToken ct) { if (PlayerCharacter == IntPtr.Zero) return; _cachedData.GlamourerData.TryGetValue(objectKind, out var glamourerData); if (objectKind == ObjectKind.Player) { - _dalamudUtil.WaitWhileCharacterIsDrawing(PlayerCharacter); + _dalamudUtil.WaitWhileCharacterIsDrawing(PlayerName!, PlayerCharacter, ct); + ct.ThrowIfCancellationRequested(); + _ipcManager.HeelsSetOffsetForPlayer(_cachedData.HeelsOffset, PlayerCharacter); RequestedPenumbraRedraw = true; Logger.Debug( $"Request Redraw for {PlayerName}"); @@ -250,6 +254,8 @@ public class CachedPlayer if (minionOrMount != null) { Logger.Debug($"Request Redraw for Minion/Mount"); + _dalamudUtil.WaitWhileCharacterIsDrawing(PlayerName! + " minion or mount", (IntPtr)minionOrMount, ct); + ct.ThrowIfCancellationRequested(); if (_ipcManager.CheckGlamourerApi() && !string.IsNullOrEmpty(glamourerData)) { _ipcManager.GlamourerApplyAll(glamourerData, obj: (IntPtr)minionOrMount); @@ -262,17 +268,29 @@ public class CachedPlayer } else if (objectKind == ObjectKind.Pet) { + int tick = 16; var pet = _dalamudUtil.GetPet(PlayerCharacter); if (pet != IntPtr.Zero) { - Logger.Debug("Request Redraw for Pet"); + var totalWait = 0; + var newPet = IntPtr.Zero; + const int maxWait = 3000; + Logger.Debug($"Request Redraw for Pet, waiting {maxWait}ms"); + + do + { + Thread.Sleep(tick); + totalWait += tick; + newPet = _dalamudUtil.GetPet(PlayerCharacter); + } while (newPet == pet && totalWait < maxWait); + if (_ipcManager.CheckGlamourerApi() && !string.IsNullOrEmpty(glamourerData)) { - _ipcManager.GlamourerApplyAll(glamourerData, pet); + _ipcManager.GlamourerApplyAll(glamourerData, newPet); } else { - _ipcManager.PenumbraRedraw(pet); + _ipcManager.PenumbraRedraw(newPet); } } } @@ -282,6 +300,8 @@ public class CachedPlayer if (companion != IntPtr.Zero) { Logger.Debug("Request Redraw for Companion"); + _dalamudUtil.WaitWhileCharacterIsDrawing(PlayerName! + " companion", companion, ct); + ct.ThrowIfCancellationRequested(); if (_ipcManager.CheckGlamourerApi() && !string.IsNullOrEmpty(glamourerData)) { _ipcManager.GlamourerApplyAll(glamourerData, companion); @@ -348,6 +368,8 @@ public class CachedPlayer _dalamudUtil.DelayedFrameworkUpdate -= DalamudUtilOnDelayedFrameworkUpdate; _ipcManager.PenumbraRedrawEvent -= IpcManagerOnPenumbraRedrawEvent; _ipcManager.PenumbraRemoveTemporaryCollection(PlayerName); + _downloadCancellationTokenSource?.Cancel(); + _downloadCancellationTokenSource?.Dispose(); if (PlayerCharacter != IntPtr.Zero) { foreach (var item in _cachedData.FileReplacements) @@ -355,9 +377,6 @@ public class CachedPlayer RevertCustomizationData(item.Key); } } - - _downloadCancellationTokenSource?.Cancel(); - _downloadCancellationTokenSource?.Dispose(); } catch (Exception ex) { @@ -374,6 +393,7 @@ public class CachedPlayer public void InitializePlayer(IntPtr character, string name, CharacterCacheDto? cache) { + if (!_isDisposed) return; Logger.Debug("Initializing Player " + this + " has cache: " + (cache != null)); IsVisible = true; PlayerName = name; @@ -426,14 +446,16 @@ public class CachedPlayer _penumbraRedrawEventTask = Task.Run(() => { PlayerCharacter = address; - using var cts = new CancellationTokenSource(); + var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromSeconds(5)); + _dalamudUtil.WaitWhileCharacterIsDrawing(PlayerName!, PlayerCharacter, cts.Token); + cts.Dispose(); + cts = new CancellationTokenSource(); cts.CancelAfter(TimeSpan.FromSeconds(5)); - _dalamudUtil.WaitWhileCharacterIsDrawing(PlayerCharacter, cts.Token); - if (RequestedPenumbraRedraw == false) { Logger.Debug("Unauthorized character change detected"); - ApplyCustomizationData(ObjectKind.Player); + ApplyCustomizationData(ObjectKind.Player, cts.Token); } else { @@ -441,6 +463,7 @@ public class CachedPlayer Logger.Debug( $"Penumbra Redraw done for {PlayerName}"); } + cts.Dispose(); }); } diff --git a/MareSynchronos/Managers/FileCacheManager.cs b/MareSynchronos/Managers/FileCacheManager.cs index b1d75f4..b8ffcac 100644 --- a/MareSynchronos/Managers/FileCacheManager.cs +++ b/MareSynchronos/Managers/FileCacheManager.cs @@ -23,6 +23,7 @@ namespace MareSynchronos.Managers private readonly CancellationTokenSource _rescanTaskCancellationTokenSource = new(); private CancellationTokenSource _rescanTaskRunCancellationTokenSource = new(); private CancellationTokenSource? _scanCancellationTokenSource; + private object modifiedFilesLock = new object(); public FileCacheManager(IpcManager ipcManager, Configuration pluginConfiguration) { Logger.Verbose("Creating " + nameof(FileCacheManager)); @@ -48,7 +49,7 @@ namespace MareSynchronos.Managers public string WatchedPenumbraDirectory => (_penumbraDirWatcher?.EnableRaisingEvents ?? false) ? _penumbraDirWatcher!.Path : "Not watched"; - public FileCache? Create(string file, CancellationToken token) + public FileCache? Create(string file, CancellationToken? token) { FileInfo fileInfo = new(file); int attempt = 0; @@ -56,7 +57,7 @@ namespace MareSynchronos.Managers { Thread.Sleep(1000); Logger.Debug("Waiting for file release " + fileInfo.FullName + " attempt " + attempt); - token.ThrowIfCancellationRequested(); + token?.ThrowIfCancellationRequested(); } if (attempt >= 10) return null; @@ -64,7 +65,7 @@ namespace MareSynchronos.Managers var sha1Hash = Crypto.GetFileHash(fileInfo.FullName); return new FileCache() { - Filepath = fileInfo.FullName.ToLower(), + Filepath = fileInfo.FullName.ToLowerInvariant(), Hash = sha1Hash, LastModifiedDate = fileInfo.LastWriteTimeUtc.Ticks.ToString(), }; @@ -142,7 +143,10 @@ namespace MareSynchronos.Managers private void OnModified(object sender, FileSystemEventArgs e) { - _modifiedFiles.Add(e.FullPath); + lock (modifiedFilesLock) + { + _modifiedFiles.Add(e.FullPath); + } _ = StartRescan(); } @@ -189,21 +193,28 @@ namespace MareSynchronos.Managers Logger.Debug("File changes detected"); - if (!_modifiedFiles.Any()) return; + lock (modifiedFilesLock) + { + if (!_modifiedFiles.Any()) return; + } _rescanTask = Task.Run(async () => { - var listCopy = _modifiedFiles.ToList(); - _modifiedFiles.Clear(); + List modifiedFilesCopy = new List(); + lock (modifiedFilesLock) + { + modifiedFilesCopy = _modifiedFiles.ToList(); + _modifiedFiles.Clear(); + } await using var db = new FileCacheContext(); - foreach (var item in listCopy.Distinct()) + foreach (var item in modifiedFilesCopy.Distinct()) { var fi = new FileInfo(item); if (!fi.Exists) { PluginLog.Verbose("Removed: " + item); - db.RemoveRange(db.FileCaches.Where(f => f.Filepath.ToLower() == item.ToLower())); + db.RemoveRange(db.FileCaches.Where(f => f.Filepath.ToLower() == item.ToLowerInvariant())); } else { @@ -211,7 +222,7 @@ namespace MareSynchronos.Managers var fileCache = Create(item, _rescanTaskCancellationTokenSource.Token); if (fileCache != null) { - db.RemoveRange(db.FileCaches.Where(f => f.Filepath.ToLower() == fileCache.Filepath.ToLower())); + db.RemoveRange(db.FileCaches.Where(f => f.Filepath.ToLower() == fileCache.Filepath.ToLowerInvariant())); await db.AddAsync(fileCache, _rescanTaskCancellationTokenSource.Token); } } diff --git a/MareSynchronos/Managers/IpcManager.cs b/MareSynchronos/Managers/IpcManager.cs index c545764..e0f63c1 100644 --- a/MareSynchronos/Managers/IpcManager.cs +++ b/MareSynchronos/Managers/IpcManager.cs @@ -11,6 +11,8 @@ using System.Collections.Concurrent; namespace MareSynchronos.Managers { public delegate void PenumbraRedrawEvent(IntPtr address, int objTblIdx); + public delegate void HeelsOffsetChange(float change); + public delegate void PenumbraResourceLoadEvent(IntPtr drawObject, string gamePath, string filePath); public class IpcManager : IDisposable { private readonly ICallGateSubscriber _glamourerApiVersion; @@ -33,6 +35,14 @@ namespace MareSynchronos.Managers private readonly ICallGateSubscriber? _reverseResolvePlayer; private readonly ICallGateSubscriber, string, int, int> _penumbraSetTemporaryMod; + private readonly ICallGateSubscriber _penumbraGameObjectResourcePathResolved; + + private readonly ICallGateSubscriber _heelsGetApiVersion; + private readonly ICallGateSubscriber _heelsGetOffset; + private readonly ICallGateSubscriber _heelsOffsetUpdate; + private readonly ICallGateSubscriber _heelsRegisterPlayer; + private readonly ICallGateSubscriber _heelsUnregisterPlayer; + private readonly DalamudUtil _dalamudUtil; private readonly ConcurrentQueue actionQueue = new(); @@ -58,7 +68,9 @@ namespace MareSynchronos.Managers _glamourerApplyOnlyCustomization = pi.GetIpcSubscriber("Glamourer.ApplyOnlyCustomizationToCharacter"); _glamourerApplyOnlyEquipment = pi.GetIpcSubscriber("Glamourer.ApplyOnlyEquipmentToCharacter"); _glamourerRevertCustomization = pi.GetIpcSubscriber("Glamourer.RevertCharacter"); + _penumbraGameObjectResourcePathResolved = pi.GetIpcSubscriber("Penumbra.GameObjectResourcePathResolved"); + _penumbraGameObjectResourcePathResolved.Subscribe(ResourceLoaded); _penumbraObjectIsRedrawn.Subscribe(RedrawEvent); _penumbraInit.Subscribe(PenumbraInit); _penumbraDispose.Subscribe(PenumbraDispose); @@ -82,6 +94,15 @@ namespace MareSynchronos.Managers _dalamudUtil.FrameworkUpdate += HandleActionQueue; } + private void ResourceLoaded(IntPtr ptr, string arg1, string arg2) + { + if (ptr != IntPtr.Zero && string.Compare(arg1, arg2, true, System.Globalization.CultureInfo.InvariantCulture) != 0) + { + PenumbraResourceLoadEvent?.Invoke(ptr, arg1, arg2); + //Logger.Debug($"Resolved {ptr:X}: {arg1} => {arg2}"); + } + } + private void HandleActionQueue() { if (actionQueue.TryDequeue(out var action)) @@ -95,6 +116,8 @@ namespace MareSynchronos.Managers public event VoidDelegate? PenumbraInitialized; public event VoidDelegate? PenumbraDisposed; public event PenumbraRedrawEvent? PenumbraRedrawEvent; + public event HeelsOffsetChange? HeelsOffsetChangeEvent; + public event PenumbraResourceLoadEvent? PenumbraResourceLoadEvent; public bool Initialized => CheckPenumbraApi(); public bool CheckGlamourerApi() @@ -113,7 +136,7 @@ namespace MareSynchronos.Managers { try { - return _penumbraApiVersion.InvokeFunc() is { Item1: 4, Item2: >= 11 }; + return _penumbraApiVersion.InvokeFunc() is { Item1: 4, Item2: >= 13 }; } catch { @@ -125,12 +148,43 @@ namespace MareSynchronos.Managers { Logger.Verbose("Disposing " + nameof(IpcManager)); + int totalSleepTime = 0; + while (actionQueue.Count > 0 && totalSleepTime < 2000) + { + Logger.Verbose("Waiting for actionqueue to clear..."); + System.Threading.Thread.Sleep(16); + totalSleepTime += 16; + } + + Logger.Verbose("Action queue clear or not, disposing"); _dalamudUtil.FrameworkUpdate -= HandleActionQueue; actionQueue.Clear(); _penumbraDispose.Unsubscribe(PenumbraDispose); _penumbraInit.Unsubscribe(PenumbraInit); _penumbraObjectIsRedrawn.Unsubscribe(RedrawEvent); + _penumbraGameObjectResourcePathResolved.Unsubscribe(ResourceLoaded); + _heelsOffsetUpdate.Unsubscribe(HeelsOffsetChange); + } + + public float GetHeelsOffset() + { + if (!CheckHeelsApi()) return 0.0f; + return _heelsGetOffset.InvokeFunc(); + } + + public void HeelsSetOffsetForPlayer(float offset, IntPtr character) + { + if(!CheckHeelsApi()) return; + actionQueue.Enqueue(() => + { + var gameObj = _dalamudUtil.CreateGameObject(character); + if (gameObj != null) + { + Logger.Verbose("Applying Heels data to " + character.ToString("X")); + _heelsRegisterPlayer.InvokeAction(gameObj, offset); + } + }); } public void GlamourerApplyAll(string? customization, IntPtr obj) @@ -246,11 +300,11 @@ namespace MareSynchronos.Managers }); } - public string? PenumbraResolvePath(string path) + public string PenumbraResolvePath(string path) { - if (!CheckPenumbraApi()) return null; + if (!CheckPenumbraApi()) return path; var resolvedPath = _penumbraResolvePlayer!.InvokeFunc(path); - return resolvedPath; + return resolvedPath ?? path; } public string[] PenumbraReverseResolvePlayer(string path) diff --git a/MareSynchronos/Managers/OnlinePlayerManager.cs b/MareSynchronos/Managers/OnlinePlayerManager.cs index e98aea4..d2671f8 100644 --- a/MareSynchronos/Managers/OnlinePlayerManager.cs +++ b/MareSynchronos/Managers/OnlinePlayerManager.cs @@ -9,7 +9,6 @@ using MareSynchronos.API; using MareSynchronos.Utils; using MareSynchronos.WebAPI; using MareSynchronos.WebAPI.Utils; -using Newtonsoft.Json; namespace MareSynchronos.Managers; @@ -199,30 +198,6 @@ public class OnlinePlayerManager : IDisposable return; } - if (_dalamudUtil.IsInGpose) - { - _playerTokenDisposal.TryGetValue(cachedPlayer, out var cancellationTokenSource); - cancellationTokenSource?.Cancel(); - cachedPlayer.IsVisible = false; - _playerTokenDisposal[cachedPlayer] = new CancellationTokenSource(); - cancellationTokenSource = _playerTokenDisposal[cachedPlayer]; - var token = cancellationTokenSource.Token; - Task.Run(async () => - { - Logger.Verbose("Cannot dispose Player, in GPose"); - while (_dalamudUtil.IsInGpose) - { - await Task.Delay(TimeSpan.FromSeconds(0.5)); - if (token.IsCancellationRequested) return; - } - - cachedPlayer.DisposePlayer(); - _onlineCachedPlayers.TryRemove(characterHash, out _); - }, token); - - return; - } - cachedPlayer.DisposePlayer(); _onlineCachedPlayers.TryRemove(characterHash, out _); } diff --git a/MareSynchronos/Managers/PlayerManager.cs b/MareSynchronos/Managers/PlayerManager.cs index a978fb5..5bb7ba4 100644 --- a/MareSynchronos/Managers/PlayerManager.cs +++ b/MareSynchronos/Managers/PlayerManager.cs @@ -9,6 +9,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character; using System.Collections.Generic; using System.Linq; using MareSynchronos.Models; +using Newtonsoft.Json; namespace MareSynchronos.Managers { @@ -19,6 +20,7 @@ namespace MareSynchronos.Managers private readonly ApiController _apiController; private readonly CharacterDataFactory _characterDataFactory; private readonly DalamudUtil _dalamudUtil; + private readonly TransientResourceManager _transientResourceManager; private readonly IpcManager _ipcManager; public event PlayerHasChanged? PlayerHasChanged; public CharacterCacheDto? LastCreatedCharacterData { get; private set; } @@ -30,7 +32,7 @@ namespace MareSynchronos.Managers private List playerRelatedObjects = new List(); public unsafe PlayerManager(ApiController apiController, IpcManager ipcManager, - CharacterDataFactory characterDataFactory, DalamudUtil dalamudUtil) + CharacterDataFactory characterDataFactory, DalamudUtil dalamudUtil, TransientResourceManager transientResourceManager) { Logger.Verbose("Creating " + nameof(PlayerManager)); @@ -38,9 +40,10 @@ namespace MareSynchronos.Managers _ipcManager = ipcManager; _characterDataFactory = characterDataFactory; _dalamudUtil = dalamudUtil; - + _transientResourceManager = transientResourceManager; _apiController.Connected += ApiControllerOnConnected; _apiController.Disconnected += ApiController_Disconnected; + _transientResourceManager.TransientResourceLoaded += HandleTransientResourceLoad; _dalamudUtil.DelayedFrameworkUpdate += DalamudUtilOnDelayedFrameworkUpdate; Logger.Debug("Watching Player, ApiController is Connected: " + _apiController.IsConnected); @@ -58,6 +61,29 @@ namespace MareSynchronos.Managers }; } + public void HandleTransientResourceLoad(IntPtr gameObj) + { + foreach (var obj in playerRelatedObjects) + { + if (obj.Address == gameObj && !obj.HasUnprocessedUpdate) + { + obj.HasUnprocessedUpdate = true; + OnPlayerOrAttachedObjectsChanged(); + return; + } + } + } + + private void HeelsOffsetChanged(float change) + { + var player = playerRelatedObjects.First(f => f.ObjectKind == ObjectKind.Player); + if (LastCreatedCharacterData != null && LastCreatedCharacterData.HeelsOffset != change && !player.IsProcessing) + { + Logger.Debug("Heels offset changed to " + change); + playerRelatedObjects.First(f => f.ObjectKind == ObjectKind.Player).HasUnprocessedUpdate = true; + } + } + public void Dispose() { Logger.Verbose("Disposing " + nameof(PlayerManager)); @@ -67,6 +93,11 @@ namespace MareSynchronos.Managers _ipcManager.PenumbraRedrawEvent -= IpcManager_PenumbraRedrawEvent; _dalamudUtil.DelayedFrameworkUpdate -= DalamudUtilOnDelayedFrameworkUpdate; + + _transientResourceManager.TransientResourceLoaded -= HandleTransientResourceLoad; + + _playerChangedCts?.Cancel(); + _ipcManager.HeelsOffsetChangeEvent -= HeelsOffsetChanged; } private unsafe void DalamudUtilOnDelayedFrameworkUpdate() @@ -110,6 +141,7 @@ namespace MareSynchronos.Managers while (!PermanentDataCache.IsReady && !token.IsCancellationRequested) { + Logger.Verbose("Waiting until cache is ready"); await Task.Delay(50, token); } @@ -117,7 +149,9 @@ namespace MareSynchronos.Managers Logger.Verbose("Cache creation complete"); - return PermanentDataCache.ToCharacterCacheDto(); + var cache = PermanentDataCache.ToCharacterCacheDto(); + //Logger.Verbose(JsonConvert.SerializeObject(cache, Formatting.Indented)); + return cache; } private void IpcManager_PenumbraRedrawEvent(IntPtr address, int idx) @@ -129,6 +163,7 @@ namespace MareSynchronos.Managers if (address == item.Address) { Logger.Debug("Penumbra redraw Event for " + item.ObjectKind); + //_transientResourceManager.CleanSemiTransientResources(item.ObjectKind); item.HasUnprocessedUpdate = true; } } @@ -141,8 +176,6 @@ namespace MareSynchronos.Managers private void OnPlayerOrAttachedObjectsChanged() { - if (_dalamudUtil.IsInGpose) return; - var unprocessedObjects = playerRelatedObjects.Where(c => c.HasUnprocessedUpdate).ToList(); foreach (var unprocessedObject in unprocessedObjects) { @@ -176,7 +209,10 @@ namespace MareSynchronos.Managers Task.Run(async () => { - _dalamudUtil.WaitWhileSelfIsDrawing(token); + foreach(var item in unprocessedObjects) + { + _dalamudUtil.WaitWhileCharacterIsDrawing("self " + item.ObjectKind.ToString(), item.Address, token); + } CharacterCacheDto? cacheDto = (await CreateFullCharacterCacheDto(token)); if (cacheDto == null || token.IsCancellationRequested) return; diff --git a/MareSynchronos/Managers/TransientResourceManager.cs b/MareSynchronos/Managers/TransientResourceManager.cs new file mode 100644 index 0000000..7bc3690 --- /dev/null +++ b/MareSynchronos/Managers/TransientResourceManager.cs @@ -0,0 +1,175 @@ +using MareSynchronos.API; +using MareSynchronos.Models; +using MareSynchronos.Utils; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MareSynchronos.Managers +{ + public delegate void TransientResourceLoadedEvent(IntPtr drawObject); + + public class TransientResourceManager : IDisposable + { + private readonly IpcManager manager; + private readonly DalamudUtil dalamudUtil; + + public event TransientResourceLoadedEvent? TransientResourceLoaded; + + private Dictionary> TransientResources { get; } = new(); + private Dictionary> SemiTransientResources { get; } = new(); + public TransientResourceManager(IpcManager manager, DalamudUtil dalamudUtil) + { + manager.PenumbraResourceLoadEvent += Manager_PenumbraResourceLoadEvent; + this.manager = manager; + this.dalamudUtil = dalamudUtil; + dalamudUtil.FrameworkUpdate += DalamudUtil_FrameworkUpdate; + dalamudUtil.ClassJobChanged += DalamudUtil_ClassJobChanged; + } + + private void DalamudUtil_ClassJobChanged() + { + if (SemiTransientResources.ContainsKey(ObjectKind.Pet)) + { + SemiTransientResources[ObjectKind.Pet].Clear(); + } + } + + private void DalamudUtil_FrameworkUpdate() + { + foreach (var item in TransientResources.ToList()) + { + if (!dalamudUtil.IsGameObjectPresent(item.Key)) + { + Logger.Debug("Object not present anymore: " + item.Key.ToString("X")); + TransientResources.Remove(item.Key); + } + } + } + + public void CleanSemiTransientResources(ObjectKind objectKind) + { + if (SemiTransientResources.ContainsKey(objectKind)) + { + SemiTransientResources[objectKind].Clear(); + } + } + + public List GetTransientResources(IntPtr gameObject) + { + if (TransientResources.TryGetValue(gameObject, out var result)) + { + return result.ToList(); + } + + return new List(); + } + + public List GetSemiTransientResources(ObjectKind objectKind) + { + if (SemiTransientResources.TryGetValue(objectKind, out var result)) + { + return result.ToList(); + } + + return new List(); + } + + private void Manager_PenumbraResourceLoadEvent(IntPtr gameObject, string gamePath, string filePath) + { + if (!TransientResources.ContainsKey(gameObject)) + { + TransientResources[gameObject] = new(); + } + + if (filePath.StartsWith("|")) + { + filePath = filePath.Split("|")[2]; + } + + filePath = filePath.ToLowerInvariant(); + + var replacedGamePath = gamePath.ToLowerInvariant().Replace("\\", "/"); + + if (TransientResources[gameObject].Contains(replacedGamePath) || + SemiTransientResources.Any(r => r.Value.Any(f => f.GamePaths.First().ToLowerInvariant() == replacedGamePath + && f.ResolvedPath.ToLowerInvariant() == filePath))) + { + Logger.Debug("Not adding " + replacedGamePath + ":" + filePath); + Logger.Verbose("SemiTransientAny: " + SemiTransientResources.Any(r => r.Value.Any(f => f.GamePaths.First().ToLowerInvariant() == replacedGamePath + && f.ResolvedPath.ToLowerInvariant() == filePath)).ToString() + ", TransientAny: " + TransientResources[gameObject].Contains(replacedGamePath)); + } + else + { + TransientResources[gameObject].Add(replacedGamePath); + Logger.Debug($"Adding {replacedGamePath} for {gameObject} ({filePath})"); + TransientResourceLoaded?.Invoke(gameObject); + } + } + + public void RemoveTransientResource(IntPtr gameObject, FileReplacement fileReplacement) + { + if (TransientResources.ContainsKey(gameObject)) + { + TransientResources[gameObject].RemoveWhere(f => fileReplacement.ResolvedPath.ToLowerInvariant() == f.ToLowerInvariant()); + } + } + + public void PersistTransientResources(IntPtr gameObject, ObjectKind objectKind, Func createFileReplacement) + { + if (!SemiTransientResources.ContainsKey(objectKind)) + { + SemiTransientResources[objectKind] = new HashSet(); + } + + if (!TransientResources.TryGetValue(gameObject, out var resources)) + { + return; + } + + var transientResources = resources.ToList(); + Logger.Debug("Persisting " + transientResources.Count + " transient resources"); + foreach (var item in transientResources) + { + var existingResource = SemiTransientResources[objectKind].Any(f => f.GamePaths.First().ToLowerInvariant() == item.ToLowerInvariant()); + if (existingResource) + { + Logger.Debug("Semi Transient resource replaced: " + item); + SemiTransientResources[objectKind].RemoveWhere(f => f.GamePaths.First().ToLowerInvariant() == item.ToLowerInvariant()); + } + + 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); + } + } + + TransientResources[gameObject].Clear(); + } + + public void Dispose() + { + dalamudUtil.FrameworkUpdate -= DalamudUtil_FrameworkUpdate; + manager.PenumbraResourceLoadEvent -= Manager_PenumbraResourceLoadEvent; + dalamudUtil.ClassJobChanged -= DalamudUtil_ClassJobChanged; + TransientResources.Clear(); + } + + internal void AddSemiTransientResource(ObjectKind objectKind, FileReplacement item) + { + if (!SemiTransientResources.ContainsKey(objectKind)) + { + SemiTransientResources[objectKind] = new HashSet(); + } + + if (!SemiTransientResources[objectKind].Any(f => f.ResolvedPath.ToLowerInvariant() == item.ResolvedPath.ToLowerInvariant())) + { + SemiTransientResources[objectKind].Add(item); + } + } + } +} diff --git a/MareSynchronos/MareSynchronos.csproj b/MareSynchronos/MareSynchronos.csproj index 2da6165..277faca 100644 --- a/MareSynchronos/MareSynchronos.csproj +++ b/MareSynchronos/MareSynchronos.csproj @@ -3,7 +3,7 @@ - 0.3.15 + 0.4.0 https://github.com/Penumbra-Sync/client diff --git a/MareSynchronos/Models/CharacterData.cs b/MareSynchronos/Models/CharacterData.cs index 4a76b7a..27726cf 100644 --- a/MareSynchronos/Models/CharacterData.cs +++ b/MareSynchronos/Models/CharacterData.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Linq; using System.Text; using MareSynchronos.API; +using MareSynchronos.Utils; +using Lumina.Excel.GeneratedSheets; namespace MareSynchronos.Models { @@ -39,16 +41,31 @@ namespace MareSynchronos.Models public CharacterCacheDto ToCharacterCacheDto() { + var fileReplacements = FileReplacements.ToDictionary(k => k.Key, k => k.Value.Where(f => f.HasFileReplacement && !f.IsFileSwap).GroupBy(f => f.Hash).Select(g => + { + return new FileReplacementDto() + { + GamePaths = g.SelectMany(g => g.GamePaths).Distinct().ToArray(), + Hash = g.First().Hash, + }; + }).ToList()); + + Logger.Debug("Adding fileSwaps"); + foreach (var item in FileReplacements) + { + Logger.Debug("Checking fileSwaps for " + item.Key); + var fileSwapsToAdd = item.Value.Where(f => f.IsFileSwap).Select(f => f.ToFileReplacementDto()); + Logger.Debug("Adding " + fileSwapsToAdd.Count() + " file swaps"); + foreach (var swap in fileSwapsToAdd) + { + Logger.Debug("Adding: " + swap.GamePaths.First() + ":" + swap.FileSwapPath); + } + fileReplacements[item.Key].AddRange(fileSwapsToAdd); + } + return new CharacterCacheDto() { - 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()), + FileReplacements = fileReplacements, GlamourerData = GlamourerString.ToDictionary(d => d.Key, d => d.Value), ManipulationData = ManipulationString }; diff --git a/MareSynchronos/Models/FileReplacement.cs b/MareSynchronos/Models/FileReplacement.cs index a9b696b..3d82d00 100644 --- a/MareSynchronos/Models/FileReplacement.cs +++ b/MareSynchronos/Models/FileReplacement.cs @@ -8,6 +8,7 @@ using MareSynchronos.FileCacheDB; using System.IO; using MareSynchronos.API; using MareSynchronos.Utils; +using System.Text.RegularExpressions; namespace MareSynchronos.Models { @@ -20,27 +21,29 @@ namespace MareSynchronos.Models _penumbraDirectory = penumbraDirectory; } - public bool Computed => !HasFileReplacement || !string.IsNullOrEmpty(Hash); + public bool Computed => IsFileSwap || !HasFileReplacement || !string.IsNullOrEmpty(Hash); public List GamePaths { get; set; } = new(); public bool HasFileReplacement => GamePaths.Count >= 1 && GamePaths.Any(p => p != ResolvedPath); + public bool IsFileSwap => !Regex.IsMatch(ResolvedPath, @"^[a-zA-Z]:(/|\\)", RegexOptions.ECMAScript) && GamePaths.First() != ResolvedPath; + public string Hash { get; set; } = string.Empty; - + public string ResolvedPath { get; set; } = string.Empty; - + public void SetResolvedPath(string path) { - ResolvedPath = path.ToLower().Replace('/', '\\').Replace(_penumbraDirectory, "").Replace('\\', '/'); - if (!HasFileReplacement) return; + ResolvedPath = path.ToLowerInvariant().Replace('/', '\\').Replace(_penumbraDirectory, "").Replace('\\', '/'); + if (!HasFileReplacement || IsFileSwap) return; _ = Task.Run(() => { FileCache? fileCache; using (FileCacheContext db = new()) { - fileCache = db.FileCaches.FirstOrDefault(f => f.Filepath == path.ToLower()); + fileCache = db.FileCaches.FirstOrDefault(f => f.Filepath == path.ToLowerInvariant()); } if (fileCache != null) @@ -54,7 +57,7 @@ namespace MareSynchronos.Models { Hash = ComputeHash(fi); using var db = new FileCacheContext(); - var newTempCache = db.FileCaches.Single(f => f.Filepath == path.ToLower()); + var newTempCache = db.FileCaches.Single(f => f.Filepath == path.ToLowerInvariant()); newTempCache.Hash = Hash; db.Update(newTempCache); db.SaveChanges(); @@ -73,6 +76,7 @@ namespace MareSynchronos.Models { GamePaths = GamePaths.ToArray(), Hash = Hash, + FileSwapPath = IsFileSwap ? ResolvedPath : string.Empty }; } public override string ToString() @@ -88,15 +92,16 @@ namespace MareSynchronos.Models string hash = Crypto.GetFileHash(fi.FullName); using FileCacheContext db = new(); - var fileAddedDuringCompute = db.FileCaches.FirstOrDefault(f => f.Filepath == fi.FullName.ToLower()); + var fileAddedDuringCompute = db.FileCaches.FirstOrDefault(f => f.Filepath == fi.FullName.ToLowerInvariant()); if (fileAddedDuringCompute != null) return fileAddedDuringCompute.Hash; try { + Logger.Debug("Adding new file to DB: " + fi.FullName + ", " + hash); db.Add(new FileCache() { Hash = hash, - Filepath = fi.FullName.ToLower(), + Filepath = fi.FullName.ToLowerInvariant(), LastModifiedDate = fi.LastWriteTimeUtc.Ticks.ToString() }); db.SaveChanges(); diff --git a/MareSynchronos/Models/PlayerRelatedObject.cs b/MareSynchronos/Models/PlayerRelatedObject.cs index 39a021d..34b6ad5 100644 --- a/MareSynchronos/Models/PlayerRelatedObject.cs +++ b/MareSynchronos/Models/PlayerRelatedObject.cs @@ -19,7 +19,18 @@ namespace MareSynchronos.Models public IntPtr Address { get; set; } public IntPtr DrawObjectAddress { get; set; } - private IntPtr CurrentAddress => getAddress.Invoke(); + private IntPtr CurrentAddress + { + get + { + try + { + return getAddress.Invoke(); + } + catch + { return IntPtr.Zero; } + } + } public PlayerRelatedObject(ObjectKind objectKind, IntPtr address, IntPtr drawObjectAddress, Func getAddress) { @@ -53,7 +64,7 @@ namespace MareSynchronos.Models if (addr || equip || drawObj || nameChange) { _name = name; - Logger.Verbose(ObjectKind + " Changed: " + _name + ", now: " + curPtr + ", " + (IntPtr)chara->GameObject.DrawObject); + Logger.Verbose($"{ObjectKind} changed: {_name}, now: {curPtr:X}, {(IntPtr)chara->GameObject.DrawObject:X}"); Address = curPtr; DrawObjectAddress = (IntPtr)chara->GameObject.DrawObject; diff --git a/MareSynchronos/Plugin.cs b/MareSynchronos/Plugin.cs index 3fcf4fa..d3b9ba8 100644 --- a/MareSynchronos/Plugin.cs +++ b/MareSynchronos/Plugin.cs @@ -14,6 +14,7 @@ using Dalamud.Interface.Windowing; using MareSynchronos.UI; using MareSynchronos.Utils; using System.Runtime.InteropServices; +using Dalamud.Game.ClientState.Conditions; namespace MareSynchronos { @@ -31,6 +32,7 @@ namespace MareSynchronos private readonly SettingsUi _settingsUi; private readonly WindowSystem _windowSystem; private PlayerManager? _playerManager; + private TransientResourceManager? _transientResourceManager; private readonly DalamudUtil _dalamudUtil; private OnlinePlayerManager? _characterCacheManager; private readonly DownloadUi _downloadUi; @@ -41,7 +43,7 @@ namespace MareSynchronos public Plugin(DalamudPluginInterface pluginInterface, CommandManager commandManager, - Framework framework, ObjectTable objectTable, ClientState clientState) + Framework framework, ObjectTable objectTable, ClientState clientState, Condition condition) { Logger.Debug("Launching " + Name); PluginInterface = pluginInterface; @@ -59,7 +61,7 @@ namespace MareSynchronos new FileCacheContext().Dispose(); // make sure db is initialized I guess // those can be initialized outside of game login - _dalamudUtil = new DalamudUtil(clientState, objectTable, framework); + _dalamudUtil = new DalamudUtil(clientState, objectTable, framework, condition); _apiController = new ApiController(_configuration, _dalamudUtil); _ipcManager = new IpcManager(PluginInterface, _dalamudUtil); @@ -123,7 +125,7 @@ namespace MareSynchronos _commandManager.RemoveHandler(CommandName); _dalamudUtil.LogIn -= DalamudUtilOnLogIn; _dalamudUtil.LogOut -= DalamudUtilOnLogOut; - + _uiSharedComponent.Dispose(); _settingsUi?.Dispose(); _introUi?.Dispose(); @@ -134,6 +136,8 @@ namespace MareSynchronos _ipcManager?.Dispose(); _playerManager?.Dispose(); _characterCacheManager?.Dispose(); + _transientResourceManager?.Dispose(); + _dalamudUtil.Dispose(); Logger.Debug("Shut down"); } @@ -165,6 +169,7 @@ namespace MareSynchronos Logger.Debug("Client logout"); _characterCacheManager?.Dispose(); _playerManager?.Dispose(); + _transientResourceManager?.Dispose(); PluginInterface.UiBuilder.Draw -= Draw; PluginInterface.UiBuilder.OpenConfigUi -= OpenUi; _commandManager.RemoveHandler(CommandName); @@ -174,6 +179,7 @@ namespace MareSynchronos { _characterCacheManager?.Dispose(); _playerManager?.Dispose(); + _transientResourceManager?.Dispose(); Task.Run(WaitForPlayerAndLaunchCharacterManager); } @@ -187,10 +193,11 @@ namespace MareSynchronos try { + _transientResourceManager = new TransientResourceManager(_ipcManager, _dalamudUtil); var characterCacheFactory = - new CharacterDataFactory(_dalamudUtil, _ipcManager); + new CharacterDataFactory(_dalamudUtil, _ipcManager, _transientResourceManager); _playerManager = new PlayerManager(_apiController, _ipcManager, - characterCacheFactory, _dalamudUtil); + characterCacheFactory, _dalamudUtil, _transientResourceManager); _characterCacheManager = new OnlinePlayerManager(_framework, _apiController, _dalamudUtil, _ipcManager, _playerManager); } diff --git a/MareSynchronos/UI/CompactUI.cs b/MareSynchronos/UI/CompactUI.cs index 299c590..04a23e1 100644 --- a/MareSynchronos/UI/CompactUI.cs +++ b/MareSynchronos/UI/CompactUI.cs @@ -292,8 +292,8 @@ namespace MareSynchronos.UI { if (_characterOrCommentFilter.IsNullOrEmpty()) return true; _configuration.GetCurrentServerUidComments().TryGetValue(p.OtherUID, out var comment); - return p.OtherUID.ToLower().Contains(_characterOrCommentFilter.ToLower()) || - (comment?.ToLower().Contains(_characterOrCommentFilter.ToLower()) ?? false); + return p.OtherUID.ToLowerInvariant().Contains(_characterOrCommentFilter.ToLowerInvariant()) || + (comment?.ToLowerInvariant().Contains(_characterOrCommentFilter.ToLowerInvariant()) ?? false); }); if (_configuration.ReverseUserSort) users = users.Reverse(); diff --git a/MareSynchronos/UI/DownloadUi.cs b/MareSynchronos/UI/DownloadUi.cs index 9e9f05f..ca91914 100644 --- a/MareSynchronos/UI/DownloadUi.cs +++ b/MareSynchronos/UI/DownloadUi.cs @@ -44,6 +44,8 @@ public class DownloadUi : Window, IDisposable Flags |= ImGuiWindowFlags.NoTitleBar; Flags |= ImGuiWindowFlags.NoDecoration; + ForceMainWindow = true; + windowSystem.AddWindow(this); IsOpen = true; } diff --git a/MareSynchronos/UI/IntroUI.cs b/MareSynchronos/UI/IntroUI.cs index 6997cb9..053bf09 100644 --- a/MareSynchronos/UI/IntroUI.cs +++ b/MareSynchronos/UI/IntroUI.cs @@ -40,9 +40,10 @@ namespace MareSynchronos.UI private Dictionary _languages = new() { { "English", "en" }, { "Deutsch", "de" }, { "Français", "fr" } }; private int _currentLanguage; - private bool DarkSoulsCaptchaValid => _darkSoulsCaptcha1.Item2 == _enteredDarkSoulsCaptcha1 - && _darkSoulsCaptcha2.Item2 == _enteredDarkSoulsCaptcha2 - && _darkSoulsCaptcha3.Item2 == _enteredDarkSoulsCaptcha3; + private bool DarkSoulsCaptchaValid => _darkSoulsCaptcha1.Item2 == _enteredDarkSoulsCaptcha1.Trim() + && _darkSoulsCaptcha2.Item2 == _enteredDarkSoulsCaptcha2.Trim() + && _darkSoulsCaptcha3.Item2 == _enteredDarkSoulsCaptcha3.Trim(); + public void Dispose() { diff --git a/MareSynchronos/UI/UIShared.cs b/MareSynchronos/UI/UIShared.cs index e6226ad..6c83c57 100644 --- a/MareSynchronos/UI/UIShared.cs +++ b/MareSynchronos/UI/UIShared.cs @@ -423,7 +423,7 @@ namespace MareSynchronos.UI { if (!success) return; - _isPenumbraDirectory = path.ToLower() == _ipcManager.PenumbraModDirectory()?.ToLower(); + _isPenumbraDirectory = path.ToLowerInvariant() == _ipcManager.PenumbraModDirectory()?.ToLowerInvariant(); _isDirectoryWritable = IsDirectoryWritable(path); _cacheDirectoryHasOtherFilesThanCache = Directory.GetFiles(path, "*", SearchOption.AllDirectories).Any(f => new FileInfo(f).Name.Length != 40); _cacheDirectoryIsValidPath = Regex.IsMatch(path, @"^(?:[a-zA-Z]:\\[\w\s\-\\]+?|\/(?:[\w\s\-\/])+?)$", RegexOptions.ECMAScript); diff --git a/MareSynchronos/Utils/DalamudUtil.cs b/MareSynchronos/Utils/DalamudUtil.cs index 15e14e2..c37c6ac 100644 --- a/MareSynchronos/Utils/DalamudUtil.cs +++ b/MareSynchronos/Utils/DalamudUtil.cs @@ -2,8 +2,10 @@ using System.Collections.Generic; using System.Linq; using System.Threading; +using System.Threading.Tasks; using Dalamud.Game; using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.SubKinds; using FFXIVClientStructs.FFXIV.Client.Game.Character; @@ -15,6 +17,7 @@ namespace MareSynchronos.Utils public delegate void LogIn(); public delegate void LogOut(); + public delegate void ClassJobChanged(); public delegate void FrameworkUpdate(); @@ -23,33 +26,57 @@ namespace MareSynchronos.Utils private readonly ClientState _clientState; private readonly ObjectTable _objectTable; private readonly Framework _framework; + private readonly Condition _condition; + public event LogIn? LogIn; public event LogOut? LogOut; public event FrameworkUpdate? FrameworkUpdate; + public event ClassJobChanged? ClassJobChanged; + private uint? classJobId = 0; public event FrameworkUpdate? DelayedFrameworkUpdate; private DateTime _delayedFrameworkUpdateCheck = DateTime.Now; - public DalamudUtil(ClientState clientState, ObjectTable objectTable, Framework framework) + public unsafe bool IsGameObjectPresent(IntPtr key) + { + foreach (var obj in _objectTable) + { + if (obj.Address == key) + { + return true; + } + } + + return false; + } + + public DalamudUtil(ClientState clientState, ObjectTable objectTable, Framework framework, Condition condition) { _clientState = clientState; _objectTable = objectTable; _framework = framework; + _condition = condition; _clientState.Login += ClientStateOnLogin; _clientState.Logout += ClientStateOnLogout; _framework.Update += FrameworkOnUpdate; if (IsLoggedIn) { + classJobId = _clientState.LocalPlayer!.ClassJob.Id; ClientStateOnLogin(null, EventArgs.Empty); } } private void FrameworkOnUpdate(Framework framework) { - foreach (FrameworkUpdate frameworkInvocation in (FrameworkUpdate?.GetInvocationList() ?? Array.Empty()).Cast()) + if (_condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51] || IsInGpose) + { + return; + } + + foreach (FrameworkUpdate? frameworkInvocation in (FrameworkUpdate?.GetInvocationList() ?? Array.Empty()).Cast()) { try { - frameworkInvocation.Invoke(); + frameworkInvocation?.Invoke(); } catch (Exception ex) { @@ -58,12 +85,23 @@ namespace MareSynchronos.Utils } } - if (DateTime.Now < _delayedFrameworkUpdateCheck.AddSeconds(0.25)) return; - foreach (FrameworkUpdate frameworkInvocation in (DelayedFrameworkUpdate?.GetInvocationList() ?? Array.Empty()).Cast()) + if (DateTime.Now < _delayedFrameworkUpdateCheck.AddSeconds(1)) return; + if (_clientState.LocalPlayer != null && _clientState.LocalPlayer.IsValid()) + { + var newclassJobId = _clientState.LocalPlayer.ClassJob.Id; + + if (classJobId != newclassJobId) + { + classJobId = newclassJobId; + ClassJobChanged?.Invoke(); + } + } + + foreach (FrameworkUpdate? frameworkInvocation in (DelayedFrameworkUpdate?.GetInvocationList() ?? Array.Empty()).Cast()) { try { - frameworkInvocation.Invoke(); + frameworkInvocation?.Invoke(); } catch (Exception ex) { @@ -119,7 +157,7 @@ namespace MareSynchronos.Utils public string PlayerName => _clientState.LocalPlayer?.Name.ToString() ?? "--"; - public IntPtr PlayerPointer => _clientState.LocalPlayer!.Address; + public IntPtr PlayerPointer => _clientState.LocalPlayer?.Address ?? IntPtr.Zero; public PlayerCharacter PlayerCharacter => _clientState.LocalPlayer!; @@ -152,25 +190,33 @@ namespace MareSynchronos.Utils return null; } - public unsafe void WaitWhileCharacterIsDrawing(IntPtr characterAddress, CancellationToken? ct = null) + public async Task RunOnFrameworkThread(Func func) { - if (!_clientState.IsLoggedIn) return; + return await _framework.RunOnFrameworkThread(func); + } + + public unsafe void WaitWhileCharacterIsDrawing(string name, IntPtr characterAddress, CancellationToken? ct = null) + { + if (!_clientState.IsLoggedIn || characterAddress == IntPtr.Zero) return; var obj = (GameObject*)characterAddress; - + const int maxWaitTime = 5000; + const int tick = 250; + int curWaitTime = 0; // ReSharper disable once LoopVariableIsNeverChangedInsideLoop - while ((obj->RenderFlags & 0b100000000000) == 0b100000000000 && (!ct?.IsCancellationRequested ?? true)) // 0b100000000000 is "still rendering" or something + while ((obj->RenderFlags & 0b100000000000) == 0b100000000000 && (!ct?.IsCancellationRequested ?? true) && curWaitTime < maxWaitTime) // 0b100000000000 is "still rendering" or something { - Logger.Verbose("Waiting for character to finish drawing"); - Thread.Sleep(250); + Logger.Verbose($"Waiting for {name} to finish drawing"); + curWaitTime += tick; + Thread.Sleep(tick); } if (ct?.IsCancellationRequested ?? false) return; // wait quarter a second just in case - Thread.Sleep(250); + Thread.Sleep(tick); } - public void WaitWhileSelfIsDrawing(CancellationToken? token) => WaitWhileCharacterIsDrawing(_clientState.LocalPlayer?.Address ?? new IntPtr(), token); + public void WaitWhileSelfIsDrawing(CancellationToken? token) => WaitWhileCharacterIsDrawing("self", _clientState.LocalPlayer?.Address ?? IntPtr.Zero, token); public void Dispose() { diff --git a/MareSynchronos/Utils/Logger.cs b/MareSynchronos/Utils/Logger.cs index 0e79f43..4785077 100644 --- a/MareSynchronos/Utils/Logger.cs +++ b/MareSynchronos/Utils/Logger.cs @@ -1,11 +1,37 @@ -using System.Diagnostics; +using System; +using System.Collections.Concurrent; +using System.Diagnostics; using Dalamud.Logging; using Dalamud.Utility; +using Microsoft.Extensions.Logging; namespace MareSynchronos.Utils { - internal class Logger + [ProviderAlias("Dalamud")] + public class DalamudLoggingProvider : ILoggerProvider { + private readonly ConcurrentDictionary _loggers = + new(StringComparer.OrdinalIgnoreCase); + + public DalamudLoggingProvider() + { + } + + public ILogger CreateLogger(string categoryName) + { + return _loggers.GetOrAdd(categoryName, name => new Logger(categoryName)); + } + + public void Dispose() + { + _loggers.Clear(); + } + } + + internal class Logger : ILogger + { + private readonly string name; + public static void Info(string info) { var caller = new StackTrace().GetFrame(1)?.GetMethod()?.ReflectedType?.Name ?? "Unknown"; @@ -40,5 +66,47 @@ namespace MareSynchronos.Utils PluginLog.Verbose($"[{caller}] {verbose}"); #endif } + + public Logger(string name) + { + this.name = name; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (!IsEnabled(logLevel)) return; + + switch (logLevel) + { + case LogLevel.Debug: + PluginLog.Debug($"[{name}] [{eventId}] {formatter(state, exception)}"); + break; + case LogLevel.Error: + case LogLevel.Critical: + PluginLog.Error($"[{name}] [{eventId}] {formatter(state, exception)}"); + break; + case LogLevel.Information: + PluginLog.Information($"[{name}] [{eventId}] {formatter(state, exception)}"); + break; + case LogLevel.Warning: + PluginLog.Warning($"[{name}] [{eventId}] {formatter(state, exception)}"); + break; + case LogLevel.Trace: + default: +#if DEBUG + PluginLog.Debug($"[{name}] [{eventId}] {formatter(state, exception)}"); +#else + PluginLog.Verbose($"[{name}] {eventId} {state} {formatter(state, exception)}"); +#endif + break; + } + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public IDisposable BeginScope(TState state) => default!; } } diff --git a/MareSynchronos/WebAPI/ApIController.Functions.Files.cs b/MareSynchronos/WebAPI/ApIController.Functions.Files.cs index e1097c5..8d6c88f 100644 --- a/MareSynchronos/WebAPI/ApIController.Functions.Files.cs +++ b/MareSynchronos/WebAPI/ApIController.Functions.Files.cs @@ -76,6 +76,8 @@ namespace MareSynchronos.WebAPI List downloadFileInfoFromService = new List(); downloadFileInfoFromService.AddRange(await _mareHub!.InvokeAsync>(Api.InvokeGetFilesSizes, fileReplacementDto.Select(f => f.Hash).ToList(), ct)); + Logger.Debug("Files with size 0 or less: " + string.Join(", ", downloadFileInfoFromService.Where(f => f.Size <= 0).Select(f => f.Hash))); + CurrentDownloads[currentDownloadId] = downloadFileInfoFromService.Distinct().Select(d => new DownloadFileTransfer(d)) .Where(d => d.CanBeTransferred).ToList(); @@ -123,9 +125,13 @@ namespace MareSynchronos.WebAPI { await using (var db = new FileCacheContext()) { - allFilesInDb = CurrentDownloads[currentDownloadId] + var fileCount = CurrentDownloads[currentDownloadId] .Where(c => c.CanBeTransferred) - .All(h => db.FileCaches.Any(f => f.Hash == h.Hash)); + .Count(h => db.FileCaches.Any(f => f.Hash == h.Hash)); + var totalFiles = CurrentDownloads[currentDownloadId].Count(c => c.CanBeTransferred); + Logger.Debug("Waiting for files to be in the DB, added " + fileCount + " of " + totalFiles); + + allFilesInDb = fileCount == totalFiles; } await Task.Delay(250, ct); @@ -146,7 +152,7 @@ namespace MareSynchronos.WebAPI Logger.Verbose("New Token Created"); List unverifiedUploadHashes = new(); - foreach (var item in character.FileReplacements.SelectMany(c => c.Value.Select(v => v.Hash).Distinct()).Distinct().ToList()) + foreach (var item in character.FileReplacements.SelectMany(c => c.Value.Where(f => string.IsNullOrEmpty(f.FileSwapPath)).Select(v => v.Hash).Distinct()).Distinct().ToList()) { if (!_verifiedUploadedHashes.Contains(item)) { diff --git a/MareSynchronos/WebAPI/ApiController.Connectivity.cs b/MareSynchronos/WebAPI/ApiController.Connectivity.cs index 11c17a9..07b6706 100644 --- a/MareSynchronos/WebAPI/ApiController.Connectivity.cs +++ b/MareSynchronos/WebAPI/ApiController.Connectivity.cs @@ -11,6 +11,7 @@ using MareSynchronos.WebAPI.Utils; using Microsoft.AspNetCore.Http.Connections; using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.Logging; namespace MareSynchronos.WebAPI { @@ -304,6 +305,10 @@ namespace MareSynchronos.WebAPI options.Transports = HttpTransportType.WebSockets | HttpTransportType.ServerSentEvents | HttpTransportType.LongPolling; }) .WithAutomaticReconnect(new ForeverRetryPolicy()) + .ConfigureLogging(a => { + a.ClearProviders().AddProvider(new DalamudLoggingProvider()); + a.SetMinimumLevel(LogLevel.Trace); + }) .Build(); } @@ -334,9 +339,10 @@ namespace MareSynchronos.WebAPI { if (_mareHub is not null) { + _uploadCancellationTokenSource?.Cancel(); Logger.Info("Stopping existing connection"); _mareHub.Closed -= MareHubOnClosed; - _mareHub.Reconnecting += MareHubOnReconnecting; + _mareHub.Reconnecting -= MareHubOnReconnecting; await _mareHub.StopAsync(token); await _mareHub.DisposeAsync(); CurrentUploads.Clear();