diff --git a/MareSynchronos/Factories/CharacterDataFactory.cs b/MareSynchronos/Factories/CharacterDataFactory.cs index 6782141..d2498c8 100644 --- a/MareSynchronos/Factories/CharacterDataFactory.cs +++ b/MareSynchronos/Factories/CharacterDataFactory.cs @@ -11,6 +11,7 @@ using MareSynchronos.Utils; using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object; using Penumbra.String; using Weapon = MareSynchronos.Interop.Weapon; +using MareSynchronos.FileCache; namespace MareSynchronos.Factories; @@ -19,23 +20,23 @@ public class CharacterDataFactory private readonly DalamudUtil _dalamudUtil; private readonly IpcManager _ipcManager; private readonly TransientResourceManager _transientResourceManager; - private readonly FileReplacementFactory _fileReplacementFactory; + private readonly FileCacheManager _fileCacheManager; - public CharacterDataFactory(DalamudUtil dalamudUtil, IpcManager ipcManager, TransientResourceManager transientResourceManager, FileReplacementFactory fileReplacementFactory) + public CharacterDataFactory(DalamudUtil dalamudUtil, IpcManager ipcManager, TransientResourceManager transientResourceManager, FileCacheManager fileReplacementFactory) { Logger.Verbose("Creating " + nameof(CharacterDataFactory)); _dalamudUtil = dalamudUtil; _ipcManager = ipcManager; _transientResourceManager = transientResourceManager; - _fileReplacementFactory = fileReplacementFactory; + _fileCacheManager = fileReplacementFactory; } - private unsafe bool CheckForPointer(IntPtr playerPointer) + private unsafe bool CheckForNullDrawObject(IntPtr playerPointer) { - return playerPointer == IntPtr.Zero || ((Character*)playerPointer)->GameObject.GetDrawObject() == null; + return ((Character*)playerPointer)->GameObject.DrawObject == null; } - public CharacterData BuildCharacterData(CharacterData previousData, PlayerRelatedObject playerRelatedObject, CancellationToken token) + public CharacterData BuildCharacterData(CharacterData previousData, GameObjectHandler playerRelatedObject, CancellationToken token) { if (!_ipcManager.Initialized) { @@ -45,7 +46,16 @@ public class CharacterDataFactory bool pointerIsZero = true; try { - pointerIsZero = CheckForPointer(playerRelatedObject.Address); + pointerIsZero = playerRelatedObject.Address == IntPtr.Zero; + try + { + pointerIsZero = CheckForNullDrawObject(playerRelatedObject.Address); + } + catch + { + pointerIsZero = true; + Logger.Debug("NullRef for " + playerRelatedObject.ObjectKind); + } } catch (Exception ex) { @@ -67,17 +77,20 @@ public class CharacterDataFactory try { + pathsToForwardResolve.Clear(); + pathsToReverseResolve.Clear(); return CreateCharacterData(previousData, playerRelatedObject, token); } catch (OperationCanceledException) { Logger.Debug("Cancelled creating Character data"); + throw; } catch (Exception e) { - Logger.Warn("Failed to create " + playerRelatedObject.ObjectKind + " data"); - Logger.Warn(e.Message); - Logger.Warn(e.StackTrace ?? string.Empty); + Logger.Debug("Failed to create " + playerRelatedObject.ObjectKind + " data"); + Logger.Debug(e.Message); + Logger.Debug(e.StackTrace ?? string.Empty); } previousData.FileReplacements = previousFileReplacements; @@ -85,23 +98,7 @@ public class CharacterDataFactory return previousData; } - private (string, string) GetIndentationForInheritanceLevel(int inheritanceLevel) - { - return (string.Join("", Enumerable.Repeat("\t", inheritanceLevel)), string.Join("", Enumerable.Repeat("\t", inheritanceLevel + 2))); - } - - private void DebugPrint(FileReplacement fileReplacement, ObjectKind objectKind, string resourceType, int inheritanceLevel) - { - var indentation = GetIndentationForInheritanceLevel(inheritanceLevel); - - if (fileReplacement.HasFileReplacement) - { - Logger.Verbose(indentation.Item1 + objectKind + resourceType + " [" + string.Join(", ", fileReplacement.GamePaths) + "]"); - Logger.Verbose(indentation.Item2 + "=> " + fileReplacement.ResolvedPath); - } - } - - private unsafe void AddReplacementsFromRenderModel(RenderModel* mdl, ObjectKind objectKind, CharacterData cache, int inheritanceLevel = 0) + private unsafe void AddReplacementsFromRenderModel(RenderModel* mdl) { if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) { @@ -115,26 +112,23 @@ public class CharacterDataFactory } catch { - Logger.Warn("Could not get model data for " + objectKind); + Logger.Warn("Could not get model data"); return; } Logger.Verbose("Checking File Replacement for Model " + mdlPath); - FileReplacement mdlFileReplacement = CreateFileReplacement(mdlPath); - DebugPrint(mdlFileReplacement, objectKind, "Model", inheritanceLevel); - - cache.AddFileReplacement(objectKind, mdlFileReplacement); + AddResolvePath(mdlPath); for (var mtrlIdx = 0; mtrlIdx < mdl->MaterialCount; mtrlIdx++) { var mtrl = (Material*)mdl->Materials[mtrlIdx]; if (mtrl == null) continue; - AddReplacementsFromMaterial(mtrl, objectKind, cache, inheritanceLevel + 1); + AddReplacementsFromMaterial(mtrl); } } - private unsafe void AddReplacementsFromMaterial(Material* mtrl, ObjectKind objectKind, CharacterData cache, int inheritanceLevel = 0) + private unsafe void AddReplacementsFromMaterial(Material* mtrl) { string fileName; try @@ -144,25 +138,15 @@ public class CharacterDataFactory } catch { - Logger.Warn("Could not get material data for " + objectKind); + Logger.Warn("Could not get material data"); return; } Logger.Verbose("Checking File Replacement for Material " + fileName); var mtrlPath = fileName.Split("|")[2]; - if (cache.FileReplacements.ContainsKey(objectKind)) - { - if (cache.FileReplacements[objectKind].Any(c => c.ResolvedPath.Contains(mtrlPath, StringComparison.Ordinal))) - { - return; - } - } - var mtrlFileReplacement = CreateFileReplacement(mtrlPath); - DebugPrint(mtrlFileReplacement, objectKind, "Material", inheritanceLevel); - - cache.AddFileReplacement(objectKind, mtrlFileReplacement); + AddResolvePath(mtrlPath); var mtrlResourceHandle = (MtrlResource*)mtrl->ResourceHandle; for (var resIdx = 0; resIdx < mtrlResourceHandle->NumTex; resIdx++) @@ -181,14 +165,14 @@ public class CharacterDataFactory Logger.Verbose("Checking File Replacement for Texture " + texPath); - AddReplacementsFromTexture(texPath, objectKind, cache, inheritanceLevel + 1); + AddReplacementsFromTexture(texPath); } try { var shpkPath = "shader/sm5/shpk/" + new ByteString(mtrlResourceHandle->ShpkString).ToString(); Logger.Verbose("Checking File Replacement for Shader " + shpkPath); - AddReplacementsFromShader(shpkPath, objectKind, cache, inheritanceLevel + 1); + AddReplacementsFromShader(shpkPath); } catch { @@ -196,130 +180,92 @@ public class CharacterDataFactory } } - private void AddReplacement(string varPath, ObjectKind objectKind, CharacterData cache, int inheritanceLevel = 0, bool doNotReverseResolve = false) + private void AddReplacement(string varPath, bool doNotReverseResolve = false) { if (varPath.IsNullOrEmpty()) return; - if (cache.FileReplacements.ContainsKey(objectKind)) - { - if (cache.FileReplacements[objectKind].Any(c => c.GamePaths.Contains(varPath, StringComparer.Ordinal))) - { - return; - } - } - - var variousReplacement = CreateFileReplacement(varPath, doNotReverseResolve); - DebugPrint(variousReplacement, objectKind, "Various", inheritanceLevel); - - cache.AddFileReplacement(objectKind, variousReplacement); + AddResolvePath(varPath, doNotReverseResolve); } - private void AddReplacementsFromShader(string shpkPath, ObjectKind objectKind, CharacterData cache, int inheritanceLevel = 0) + private void AddReplacementsFromShader(string shpkPath) { if (string.IsNullOrEmpty(shpkPath)) return; - if (cache.FileReplacements.ContainsKey(objectKind)) - { - if (cache.FileReplacements[objectKind].Any(c => c.GamePaths.Contains(shpkPath, StringComparer.Ordinal))) - { - return; - } - } - - var shpkFileReplacement = CreateFileReplacement(shpkPath, doNotReverseResolve: true); - DebugPrint(shpkFileReplacement, objectKind, "Shader", inheritanceLevel); - cache.AddFileReplacement(objectKind, shpkFileReplacement); + AddResolvePath(shpkPath, doNotReverseResolve: true); } - private void AddReplacementsFromTexture(string texPath, ObjectKind objectKind, CharacterData cache, int inheritanceLevel = 0, bool doNotReverseResolve = true) + private void AddReplacementsFromTexture(string texPath, bool doNotReverseResolve = true) { if (string.IsNullOrEmpty(texPath)) return; - if (cache.FileReplacements.ContainsKey(objectKind)) - { - if (cache.FileReplacements[objectKind].Any(c => c.GamePaths.Contains(texPath, StringComparer.Ordinal))) - { - return; - } - } - - var texFileReplacement = CreateFileReplacement(texPath, doNotReverseResolve); - DebugPrint(texFileReplacement, objectKind, "Texture", inheritanceLevel); - - cache.AddFileReplacement(objectKind, texFileReplacement); + AddResolvePath(texPath, doNotReverseResolve); if (texPath.Contains("/--", StringComparison.Ordinal)) return; - var texDx11Replacement = - CreateFileReplacement(texPath.Insert(texPath.LastIndexOf('/') + 1, "--"), doNotReverseResolve); - - DebugPrint(texDx11Replacement, objectKind, "Texture (DX11)", inheritanceLevel); - - cache.AddFileReplacement(objectKind, texDx11Replacement); + AddResolvePath(texPath.Insert(texPath.LastIndexOf('/') + 1, "--"), doNotReverseResolve); } - private unsafe CharacterData CreateCharacterData(CharacterData previousData, PlayerRelatedObject playerRelatedObject, CancellationToken token) + private unsafe CharacterData CreateCharacterData(CharacterData previousData, GameObjectHandler playerRelatedObject, CancellationToken token) { var objectKind = playerRelatedObject.ObjectKind; var charaPointer = playerRelatedObject.Address; + if (!previousData.FileReplacements.ContainsKey(objectKind)) + { + previousData.FileReplacements[objectKind] = new(FileReplacementComparer.Instance); + } + + _dalamudUtil.WaitWhileCharacterIsDrawing(playerRelatedObject.ObjectKind.ToString(), playerRelatedObject.Address, ct: token); + Stopwatch st = Stopwatch.StartNew(); - if (playerRelatedObject.HasUnprocessedUpdate) + Logger.Debug("Handling unprocessed update for " + objectKind); + + if (previousData.FileReplacements.ContainsKey(objectKind)) { - Logger.Debug("Handling unprocessed update for " + objectKind); + previousData.FileReplacements[objectKind].Clear(); + } - if (previousData.FileReplacements.ContainsKey(objectKind)) + var chara = _dalamudUtil.CreateGameObject(charaPointer)!; + while (!DalamudUtil.IsObjectPresent(chara)) + { + Logger.Verbose("Character is null but it shouldn't be, waiting"); + Thread.Sleep(50); + } + + var human = (Human*)((Character*)charaPointer)->GameObject.DrawObject; + for (var mdlIdx = 0; mdlIdx < human->CharacterBase.SlotCount; ++mdlIdx) + { + var mdl = (RenderModel*)human->CharacterBase.ModelArray[mdlIdx]; + if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) { - previousData.FileReplacements[objectKind].Clear(); - } - else - { - previousData.FileReplacements.Add(objectKind, new()); + continue; } - var chara = _dalamudUtil.CreateGameObject(charaPointer)!; - while (!DalamudUtil.IsObjectPresent(chara)) - { - Logger.Verbose("Character is null but it shouldn't be, waiting"); - Thread.Sleep(50); - } + token.ThrowIfCancellationRequested(); - var human = (Human*)((Character*)charaPointer)->GameObject.GetDrawObject(); - for (var mdlIdx = 0; mdlIdx < human->CharacterBase.SlotCount; ++mdlIdx) - { - var mdl = (RenderModel*)human->CharacterBase.ModelArray[mdlIdx]; - if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) - { - continue; - } + AddReplacementsFromRenderModel(mdl); + } - token.ThrowIfCancellationRequested(); - - AddReplacementsFromRenderModel(mdl, objectKind, previousData, 0); - } + if (objectKind == ObjectKind.Player) + { + AddPlayerSpecificReplacements(objectKind, charaPointer, human); + } + if (objectKind == ObjectKind.Pet) + { foreach (var item in previousData.FileReplacements[objectKind]) { - _transientResourceManager.RemoveTransientResource(charaPointer, item); + _transientResourceManager.AddSemiTransientResource(objectKind, item.GamePaths.First()); } - if (objectKind == ObjectKind.Player) - { - AddPlayerSpecificReplacements(previousData, objectKind, charaPointer, human); - } - - if (objectKind == ObjectKind.Pet) - { - foreach (var item in previousData.FileReplacements[objectKind]) - { - _transientResourceManager.AddSemiTransientResource(objectKind, item); - } - - previousData.FileReplacements[objectKind].Clear(); - } + previousData.FileReplacements[objectKind].Clear(); } + + Dictionary> resolvedPaths = GetFileReplacementsFromPaths(); + previousData.FileReplacements[objectKind] = new HashSet(resolvedPaths.Select(c => new FileReplacement(c.Value, c.Key, _fileCacheManager))); + previousData.ManipulationString = _ipcManager.PenumbraGetMetaManipulations(); previousData.GlamourerString[objectKind] = _ipcManager.GlamourerGetCharacterCustomization(charaPointer); previousData.HeelsOffset = _ipcManager.GetHeelsOffset(); @@ -327,52 +273,75 @@ public class CharacterDataFactory previousData.PalettePlusPalette = _ipcManager.PalettePlusBuildPalette(); Logger.Debug("Handling transient update for " + objectKind); - ManageSemiTransientData(previousData, objectKind, charaPointer); + _transientResourceManager.ClearTransientPaths(charaPointer, previousData.FileReplacements[objectKind].SelectMany(c => c.GamePaths).ToList()); + + pathsToForwardResolve.Clear(); + pathsToReverseResolve.Clear(); + + ManageSemiTransientData(objectKind, charaPointer); + + var resolvedTransientPaths = GetFileReplacementsFromPaths(); + foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement(c.Value, c.Key, _fileCacheManager))) + { + previousData.FileReplacements[objectKind].Add(replacement); + } + + foreach (var item in previousData.FileReplacements[objectKind]) + { + Logger.Debug(item.ToString()); + } st.Stop(); - Logger.Verbose("Building " + objectKind + " Data took " + st.Elapsed); + Logger.Verbose("Building " + objectKind + " Data took " + st.ElapsedMilliseconds + "ms"); return previousData; } - private unsafe void ManageSemiTransientData(CharacterData previousData, ObjectKind objectKind, IntPtr charaPointer) + private Dictionary> GetFileReplacementsFromPaths() { - _transientResourceManager.PersistTransientResources(charaPointer, objectKind, CreateFileReplacement); - - // get rid of items that have no file replacements anymore - foreach (var entry in previousData.FileReplacements.ToList()) + var forwardPaths = pathsToForwardResolve.ToArray(); + var reversePaths = pathsToReverseResolve.ToArray(); + Dictionary> resolvedPaths = new(StringComparer.Ordinal); + var result = _ipcManager.PenumbraResolvePaths(pathsToForwardResolve.ToArray(), pathsToReverseResolve.ToArray()); + for (int i = 0; i < forwardPaths.Length; i++) { - foreach (var item in entry.Value.ToList()) + var filePath = result.forward[i].ToLowerInvariant(); + if (resolvedPaths.TryGetValue(filePath, out var list)) { - if (!item.HasFileReplacement) previousData.FileReplacements[entry.Key].Remove(item); + list.Add(forwardPaths[i].ToLowerInvariant()); + } + else + { + resolvedPaths[filePath] = new List { forwardPaths[i].ToLowerInvariant() }; } } + for (int i = 0; i < reversePaths.Length; i++) + { + var filePath = reversePaths[i].ToLowerInvariant(); + if (resolvedPaths.TryGetValue(filePath, out var list)) + { + list.AddRange(result.reverse[i].Select(c => c.ToLowerInvariant())); + } + else + { + resolvedPaths[filePath] = new List(result.reverse[i].Select(c => c.ToLowerInvariant()).ToList()); + } + } + + return resolvedPaths; + } + + private unsafe void ManageSemiTransientData(ObjectKind objectKind, IntPtr charaPointer) + { + _transientResourceManager.PersistTransientResources(charaPointer, objectKind); + foreach (var item in _transientResourceManager.GetSemiTransientResources(objectKind)) { - if (!previousData.FileReplacements.ContainsKey(objectKind)) - { - previousData.FileReplacements.Add(objectKind, new()); - } - - if (!previousData.FileReplacements[objectKind].Any(k => k.GamePaths.Any(p => item.GamePaths.Contains(p, StringComparer.OrdinalIgnoreCase)))) - { - var penumResolve = _ipcManager.PenumbraResolvePath(item.GamePaths.First()).ToLowerInvariant(); - var gamePath = item.GamePaths.First().ToLowerInvariant(); - if (string.Equals(penumResolve, gamePath, StringComparison.Ordinal)) - { - Logger.Verbose("PenumResolve was same as GamePath, not adding " + item); - _transientResourceManager.RemoveTransientResource(charaPointer, item); - } - else - { - Logger.Verbose("Found semi transient resource: " + item); - previousData.FileReplacements[objectKind].Add(item); - } - } + AddResolvePath(item, true); } } - private unsafe void AddPlayerSpecificReplacements(CharacterData previousData, ObjectKind objectKind, IntPtr charaPointer, Human* human) + private unsafe void AddPlayerSpecificReplacements(ObjectKind objectKind, IntPtr charaPointer, Human* human) { var weaponObject = (Weapon*)((Object*)human)->ChildObject; @@ -380,42 +349,32 @@ public class CharacterDataFactory { var mainHandWeapon = weaponObject->WeaponRenderModel->RenderModel; - AddReplacementsFromRenderModel(mainHandWeapon, objectKind, previousData, 0); - - foreach (var item in previousData.FileReplacements[objectKind]) - { - _transientResourceManager.RemoveTransientResource(charaPointer, item); - } + AddReplacementsFromRenderModel(mainHandWeapon); foreach (var item in _transientResourceManager.GetTransientResources((IntPtr)weaponObject)) { Logger.Verbose("Found transient weapon resource: " + item); - AddReplacement(item, objectKind, previousData, 1, doNotReverseResolve: true); + AddReplacement(item, doNotReverseResolve: 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); - } + AddReplacementsFromRenderModel(offHandWeapon); foreach (var item in _transientResourceManager.GetTransientResources((IntPtr)offHandWeapon)) { Logger.Verbose("Found transient offhand weapon resource: " + item); - AddReplacement(item, objectKind, previousData, 1, doNotReverseResolve: true); + AddReplacement(item, doNotReverseResolve: true); } } } - AddReplacementSkeleton(((HumanExt*)human)->Human.RaceSexId, objectKind, previousData); + AddReplacementSkeleton(((HumanExt*)human)->Human.RaceSexId); try { - AddReplacementsFromTexture(new ByteString(((HumanExt*)human)->Decal->FileName()).ToString(), objectKind, previousData, 0, doNotReverseResolve: false); + AddReplacementsFromTexture(new ByteString(((HumanExt*)human)->Decal->FileName()).ToString(), doNotReverseResolve: false); } catch { @@ -423,43 +382,29 @@ public class CharacterDataFactory } try { - AddReplacementsFromTexture(new ByteString(((HumanExt*)human)->LegacyBodyDecal->FileName()).ToString(), objectKind, previousData, 0, doNotReverseResolve: false); + AddReplacementsFromTexture(new ByteString(((HumanExt*)human)->LegacyBodyDecal->FileName()).ToString(), doNotReverseResolve: false); } catch { Logger.Warn("Could not get Legacy Body Decal Data"); } - - foreach (var item in previousData.FileReplacements[objectKind]) - { - _transientResourceManager.RemoveTransientResource(charaPointer, item); - } } - private void AddReplacementSkeleton(ushort raceSexId, ObjectKind objectKind, CharacterData cache) + private void AddReplacementSkeleton(ushort raceSexId) { string raceSexIdString = raceSexId.ToString("0000"); string skeletonPath = $"chara/human/c{raceSexIdString}/skeleton/base/b0001/skl_c{raceSexIdString}b0001.sklb"; - var replacement = CreateFileReplacement(skeletonPath, doNotReverseResolve: true); - cache.AddFileReplacement(objectKind, replacement); - - DebugPrint(replacement, objectKind, "SKLB", 0); + AddResolvePath(skeletonPath, doNotReverseResolve: true); } - private FileReplacement CreateFileReplacement(string path, bool doNotReverseResolve = false) + private void AddResolvePath(string path, bool doNotReverseResolve = false) { - var fileReplacement = _fileReplacementFactory.Create(); - if (!doNotReverseResolve) - { - fileReplacement.ReverseResolvePath(path); - } - else - { - fileReplacement.ResolvePath(path); - } - - return fileReplacement; + if (doNotReverseResolve) pathsToForwardResolve.Add(path.ToLowerInvariant()); + else pathsToReverseResolve.Add(path.ToLowerInvariant()); } + + private HashSet pathsToForwardResolve = new(StringComparer.Ordinal); + private HashSet pathsToReverseResolve = new(StringComparer.Ordinal); } diff --git a/MareSynchronos/Factories/FileReplacementFactory.cs b/MareSynchronos/Factories/FileReplacementFactory.cs deleted file mode 100644 index bf314d7..0000000 --- a/MareSynchronos/Factories/FileReplacementFactory.cs +++ /dev/null @@ -1,22 +0,0 @@ -using MareSynchronos.FileCache; -using MareSynchronos.Managers; -using MareSynchronos.Models; - -namespace MareSynchronos.Factories; - -public class FileReplacementFactory -{ - private readonly FileCacheManager fileCacheManager; - private readonly IpcManager ipcManager; - - public FileReplacementFactory(FileCacheManager fileCacheManager, IpcManager ipcManager) - { - this.fileCacheManager = fileCacheManager; - this.ipcManager = ipcManager; - } - - public FileReplacement Create() - { - return new FileReplacement(fileCacheManager, ipcManager); - } -} diff --git a/MareSynchronos/FileCache/FileCacheManager.cs b/MareSynchronos/FileCache/FileCacheManager.cs index e8168de..2661f99 100644 --- a/MareSynchronos/FileCache/FileCacheManager.cs +++ b/MareSynchronos/FileCache/FileCacheManager.cs @@ -109,8 +109,8 @@ public class FileCacheManager : IDisposable public FileCacheEntity? GetFileCacheByPath(string path) { - var cleanedPath = path.Replace("/", "\\", StringComparison.OrdinalIgnoreCase).ToLowerInvariant().Replace(_ipcManager.PenumbraModDirectory()!.ToLowerInvariant(), "", StringComparison.OrdinalIgnoreCase); - var entry = _fileCaches.FirstOrDefault(f => f.Value.ResolvedFilepath.EndsWith(cleanedPath, StringComparison.OrdinalIgnoreCase)).Value; + var cleanedPath = path.Replace("/", "\\", StringComparison.OrdinalIgnoreCase).ToLowerInvariant().Replace(_ipcManager.PenumbraModDirectory!.ToLowerInvariant(), "", StringComparison.OrdinalIgnoreCase); + var entry = _fileCaches.Values.FirstOrDefault(f => f.ResolvedFilepath.EndsWith(cleanedPath, StringComparison.OrdinalIgnoreCase)); if (entry == null) { @@ -140,8 +140,8 @@ public class FileCacheManager : IDisposable FileInfo fi = new(path); if (!fi.Exists) return null; var fullName = fi.FullName.ToLowerInvariant(); - if (!fullName.Contains(_ipcManager.PenumbraModDirectory()!.ToLowerInvariant(), StringComparison.Ordinal)) return null; - string prefixedPath = fullName.Replace(_ipcManager.PenumbraModDirectory()!.ToLowerInvariant(), _penumbraPrefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal); + if (!fullName.Contains(_ipcManager.PenumbraModDirectory!.ToLowerInvariant(), StringComparison.Ordinal)) return null; + string prefixedPath = fullName.Replace(_ipcManager.PenumbraModDirectory!.ToLowerInvariant(), _penumbraPrefix + "\\", StringComparison.Ordinal).Replace("\\\\", "\\", StringComparison.Ordinal); return CreateFileCacheEntity(fi, prefixedPath); } @@ -203,7 +203,7 @@ public class FileCacheManager : IDisposable { if (fileCache.PrefixedFilePath.StartsWith(_penumbraPrefix, StringComparison.OrdinalIgnoreCase)) { - fileCache.SetResolvedFilePath(fileCache.PrefixedFilePath.Replace(_penumbraPrefix, _ipcManager.PenumbraModDirectory(), StringComparison.Ordinal)); + fileCache.SetResolvedFilePath(fileCache.PrefixedFilePath.Replace(_penumbraPrefix, _ipcManager.PenumbraModDirectory, StringComparison.Ordinal)); } else if (fileCache.PrefixedFilePath.StartsWith(_cachePrefix, StringComparison.OrdinalIgnoreCase)) { diff --git a/MareSynchronos/FileCache/PeriodicFileScanner.cs b/MareSynchronos/FileCache/PeriodicFileScanner.cs index 5942d3d..a7c5e65 100644 --- a/MareSynchronos/FileCache/PeriodicFileScanner.cs +++ b/MareSynchronos/FileCache/PeriodicFileScanner.cs @@ -150,7 +150,7 @@ public class PeriodicFileScanner : MediatorSubscriberBase, IDisposable private void PeriodicFileScan(CancellationToken ct) { TotalFiles = 1; - var penumbraDir = _ipcManager.PenumbraModDirectory(); + var penumbraDir = _ipcManager.PenumbraModDirectory; bool penDirExists = true; bool cacheDirExists = true; if (string.IsNullOrEmpty(penumbraDir) || !Directory.Exists(penumbraDir)) diff --git a/MareSynchronos/Managers/CacheCreationService.cs b/MareSynchronos/Managers/CacheCreationService.cs new file mode 100644 index 0000000..65366fd --- /dev/null +++ b/MareSynchronos/Managers/CacheCreationService.cs @@ -0,0 +1,114 @@ +using FFXIVClientStructs.FFXIV.Client.Game.Character; +using MareSynchronos.API.Data.Enum; +using MareSynchronos.Factories; +using MareSynchronos.Mediator; +using MareSynchronos.Models; +using MareSynchronos.Utils; +using MareSynchronos.WebAPI; + +namespace MareSynchronos.Managers; + +public class CacheCreationService : MediatorSubscriberBase, IDisposable +{ + private readonly CharacterDataFactory _characterDataFactory; + private readonly IpcManager _ipcManager; + private readonly ApiController _apiController; + private Task? _cacheCreationTask; + private Dictionary _cachesToCreate = new(); + private CharacterData _lastCreatedData = new(); + private CancellationTokenSource cts = new(); + private List _playerRelatedObjects; + + public unsafe CacheCreationService(MareMediator mediator, CharacterDataFactory characterDataFactory, IpcManager ipcManager, + ApiController apiController, DalamudUtil dalamudUtil) : base(mediator) + { + _characterDataFactory = characterDataFactory; + _ipcManager = ipcManager; + _apiController = apiController; + + Mediator.Subscribe(this, (msg) => + { + var actualMsg = (CreateCacheForObjectMessage)msg; + _cachesToCreate[actualMsg.ObjectToCreateFor.ObjectKind] = actualMsg.ObjectToCreateFor; + }); + Mediator.Subscribe(this, (msg) => ProcessCacheCreation()); + Mediator.Subscribe(this, (msg) => CustomizePlusChanged((CustomizePlusMessage)msg)); + Mediator.Subscribe(this, (msg) => HeelsOffsetChanged((HeelsOffsetMessage)msg)); + Mediator.Subscribe(this, (msg) => PalettePlusChanged((PalettePlusMessage)msg)); + + _playerRelatedObjects = new List() + { + new(Mediator, ObjectKind.Player, () => dalamudUtil.PlayerPointer), + new(Mediator, ObjectKind.MinionOrMount, () => (IntPtr)((Character*)dalamudUtil.PlayerPointer)->CompanionObject), + new(Mediator, ObjectKind.Pet, () => dalamudUtil.GetPet()), + new(Mediator, ObjectKind.Companion, () => dalamudUtil.GetCompanion()), + }; + } + + private void PalettePlusChanged(PalettePlusMessage msg) + { + if (!string.Equals(msg.Data, _lastCreatedData.PalettePlusPalette, StringComparison.Ordinal)) + { + _lastCreatedData.PalettePlusPalette = msg.Data ?? string.Empty; + Mediator.Publish(new CharacterDataCreatedMessage(_lastCreatedData)); + } + } + + private void HeelsOffsetChanged(HeelsOffsetMessage msg) + { + if (msg.Offset != _lastCreatedData.HeelsOffset) + { + _lastCreatedData.HeelsOffset = msg.Offset; + Mediator.Publish(new CharacterDataCreatedMessage(_lastCreatedData)); + } + } + + private void CustomizePlusChanged(CustomizePlusMessage msg) + { + if (!string.Equals(msg.Data, _lastCreatedData.CustomizePlusScale, StringComparison.Ordinal)) + { + _lastCreatedData.CustomizePlusScale = msg.Data ?? string.Empty; + Mediator.Publish(new CharacterDataCreatedMessage(_lastCreatedData)); + } + } + + private void ProcessCacheCreation() + { + if (_cachesToCreate.Any() && (_cacheCreationTask?.IsCompleted ?? true)) + { + var toCreate = _cachesToCreate.ToList(); + _cachesToCreate.Clear(); + _cacheCreationTask = Task.Run(() => + { + try + { + foreach (var obj in toCreate) + { + var data = _characterDataFactory.BuildCharacterData(_lastCreatedData, obj.Value, cts.Token); + } + Mediator.Publish(new CharacterDataCreatedMessage(_lastCreatedData)); + } + catch (Exception ex) + { + Logger.Error("Error during Cache Creation Processing", ex); + } + finally + { + Logger.Debug("Cache Creation complete"); + + } + }, cts.Token); + } + else if (_cachesToCreate.Any()) + { + Logger.Debug("Cache Creation stored until previous creation finished"); + } + } + + public override void Dispose() + { + base.Dispose(); + _playerRelatedObjects.ForEach(p => p.Dispose()); + cts.Dispose(); + } +} diff --git a/MareSynchronos/Managers/CachedPlayer.cs b/MareSynchronos/Managers/CachedPlayer.cs index 2ccda0c..6f7c2cb 100644 --- a/MareSynchronos/Managers/CachedPlayer.cs +++ b/MareSynchronos/Managers/CachedPlayer.cs @@ -19,7 +19,7 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable private readonly IpcManager _ipcManager; private readonly FileCacheManager _fileDbManager; private API.Data.CharacterData _cachedData = new(); - private PlayerRelatedObject? _currentCharacterEquipment; + private GameObjectHandler? _currentCharacterEquipment; private CancellationTokenSource? _downloadCancellationTokenSource = new(); private bool _isVisible; @@ -65,6 +65,18 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable Logger.Debug("Checking for files to download for player " + PlayerName); Logger.Debug("Hash for data is " + characterData.DataHash.Value + ", current cache hash is " + _cachedData.DataHash.Value); + if (!_ipcManager.CheckPenumbraApi()) + { + Mediator.Publish(new NotificationMessage("Penumbra inactive", "Your Penumbra installation is not active or out of date. Update Penumbra and/or the Enable Mods setting in Penumbra to continue to use Mare.", NotificationType.Error)); + return; + } + + if (!_ipcManager.CheckGlamourerApi()) + { + Mediator.Publish(new NotificationMessage("Glamourer inactive", "Your Glamourer installation is not active or out of date. Update Glamourer to continue to use Mare.", NotificationType.Error)); + return; + } + if (string.Equals(characterData.DataHash.Value, _cachedData.DataHash.Value, StringComparison.Ordinal) && !forced) return; bool updateModdedPaths = false; @@ -116,6 +128,14 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable if (objectKind == ObjectKind.Player) { + bool manipDataDifferent = !string.Equals(_cachedData.ManipulationData, characterData.ManipulationData, StringComparison.Ordinal); + if (manipDataDifferent) + { + Logger.Debug("Updating " + objectKind); + charaDataToUpdate.Add(objectKind); + continue; + } + bool heelsOffsetDifferent = _cachedData.HeelsOffset != characterData.HeelsOffset; if (heelsOffsetDifferent) { @@ -189,8 +209,7 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable return false; } - _currentCharacterEquipment?.CheckAndUpdateObject(); - if (_currentCharacterEquipment?.HasUnprocessedUpdate ?? false) + if (_currentCharacterEquipment?.CheckAndUpdateObject() ?? false) { OnPlayerChanged(); } @@ -247,8 +266,7 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable Mediator.Subscribe(this, (msg) => IpcManagerOnPenumbraRedrawEvent(((PenumbraRedrawMessage)msg))); _originalGlamourerData = _ipcManager.GlamourerGetCharacterCustomization(PlayerCharacter); - _currentCharacterEquipment = new PlayerRelatedObject(ObjectKind.Player, IntPtr.Zero, IntPtr.Zero, - () => _dalamudUtil.GetPlayerCharacterFromObjectTableByName(PlayerName)?.Address ?? IntPtr.Zero); + _currentCharacterEquipment = new GameObjectHandler(Mediator, ObjectKind.Player, () => _dalamudUtil.GetPlayerCharacterFromObjectTableByName(PlayerName)?.Address ?? IntPtr.Zero, false); } public override string ToString() @@ -437,7 +455,7 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable { PlayerCharacter = msg.Address; var cts = new CancellationTokenSource(); - cts.CancelAfter(TimeSpan.FromSeconds(5)); + cts.CancelAfter(TimeSpan.FromSeconds(10)); _dalamudUtil.WaitWhileCharacterIsDrawing(PlayerName!, PlayerCharacter, 10000, cts.Token); cts.Dispose(); cts = new CancellationTokenSource(); @@ -460,7 +478,6 @@ public class CachedPlayer : MediatorSubscriberBase, IDisposable private void OnPlayerChanged() { Logger.Debug($"Player {PlayerName} changed, PenumbraRedraw is {RequestedPenumbraRedraw}"); - _currentCharacterEquipment!.HasUnprocessedUpdate = false; if (!RequestedPenumbraRedraw && PlayerCharacter != IntPtr.Zero) { Logger.Debug($"Saving new Glamourer data"); diff --git a/MareSynchronos/Managers/IpcManager.cs b/MareSynchronos/Managers/IpcManager.cs index 00680ff..b3858b9 100644 --- a/MareSynchronos/Managers/IpcManager.cs +++ b/MareSynchronos/Managers/IpcManager.cs @@ -35,8 +35,9 @@ public class IpcManager : MediatorSubscriberBase, IDisposable private readonly FuncSubscriber _penumbraResolvePlayer; private readonly FuncSubscriber _reverseResolvePlayer; private readonly FuncSubscriber, string, int, PenumbraApiEc> _penumbraAddTemporaryMod; + private readonly FuncSubscriber _penumbraResolvePaths; + private readonly FuncSubscriber _penumbraEnabled; private readonly EventSubscriber _penumbraGameObjectResourcePathResolved; - private readonly EventSubscriber _penumbraModSettingChanged; private readonly ICallGateSubscriber _heelsGetApiVersion; private readonly ICallGateSubscriber _heelsGetOffset; @@ -49,7 +50,7 @@ public class IpcManager : MediatorSubscriberBase, IDisposable private readonly ICallGateSubscriber _customizePlusSetBodyScaleToCharacter; private readonly ICallGateSubscriber _customizePlusRevert; private readonly ICallGateSubscriber _customizePlusOnScaleUpdate; - + private readonly ICallGateSubscriber _palettePlusApiVersion; private readonly ICallGateSubscriber _palettePlusBuildCharaPalette; private readonly ICallGateSubscriber _palettePlusSetCharaPalette; @@ -81,9 +82,10 @@ public class IpcManager : MediatorSubscriberBase, IDisposable _penumbraRemoveTemporaryCollection = Penumbra.Api.Ipc.RemoveTemporaryCollectionByName.Subscriber(pi); _penumbraRemoveTemporaryMod = Penumbra.Api.Ipc.RemoveTemporaryMod.Subscriber(pi); _penumbraAssignTemporaryCollection = Penumbra.Api.Ipc.AssignTemporaryCollection.Subscriber(pi); + _penumbraResolvePaths = Penumbra.Api.Ipc.ResolvePlayerPaths.Subscriber(pi); + _penumbraEnabled = Penumbra.Api.Ipc.GetEnabledState.Subscriber(pi); _penumbraGameObjectResourcePathResolved = Penumbra.Api.Ipc.GameObjectResourcePathResolved.Subscriber(pi, (ptr, arg1, arg2) => ResourceLoaded((IntPtr)ptr, arg1, arg2)); - _penumbraModSettingChanged = Penumbra.Api.Ipc.ModSettingChanged.Subscriber(pi, (modsetting, a, b, c) => PenumbraModSettingChangedHandler()); _glamourerApiVersion = pi.GetIpcSubscriber("Glamourer.ApiVersion"); _glamourerGetAllCustomization = pi.GetIpcSubscriber("Glamourer.GetAllCustomizationFromCharacter"); @@ -107,7 +109,7 @@ public class IpcManager : MediatorSubscriberBase, IDisposable _customizePlusOnScaleUpdate = pi.GetIpcSubscriber("CustomizePlus.OnScaleUpdate"); _customizePlusOnScaleUpdate.Subscribe(OnCustomizePlusScaleChange); - + _palettePlusApiVersion = pi.GetIpcSubscriber("PalettePlus.ApiVersion"); _palettePlusBuildCharaPalette = pi.GetIpcSubscriber("PalettePlus.BuildCharaPalette"); _palettePlusSetCharaPalette = pi.GetIpcSubscriber("PalettePlus.SetCharaPalette"); @@ -125,6 +127,12 @@ public class IpcManager : MediatorSubscriberBase, IDisposable Mediator.Subscribe(this, (_) => HandleActionQueue()); Mediator.Subscribe(this, (_) => HandleGposeActionQueue()); Mediator.Subscribe(this, (_) => ClearActionQueue()); + Mediator.Subscribe(this, (_) => CheckPenumbraModPath()); + } + + private void CheckPenumbraModPath() + { + PenumbraModDirectory = GetPenumbraModDirectory(); } private void HandleGposeActionQueue() @@ -142,11 +150,6 @@ public class IpcManager : MediatorSubscriberBase, IDisposable _inGposeQueueMode = on; } - private void PenumbraModSettingChangedHandler() - { - Mediator.Publish(new PenumbraModSettingChangedMessage()); - } - private void ClearActionQueue() { ActionQueue.Clear(); @@ -191,7 +194,7 @@ public class IpcManager : MediatorSubscriberBase, IDisposable { try { - return _penumbraApiVersion.Invoke() is { Item1: 4, Item2: >= 17 }; + return _penumbraApiVersion.Invoke() is { Item1: 4, Item2: >= 19 } && _penumbraEnabled.Invoke(); } catch { @@ -259,7 +262,6 @@ public class IpcManager : MediatorSubscriberBase, IDisposable _penumbraDispose.Dispose(); _penumbraInit.Dispose(); _penumbraObjectIsRedrawn.Dispose(); - _penumbraModSettingChanged.Dispose(); _heelsOffsetUpdate.Unsubscribe(HeelsOffsetChange); } @@ -411,7 +413,9 @@ public class IpcManager : MediatorSubscriberBase, IDisposable return _penumbraGetMetaManipulations.Invoke(); } - public string? PenumbraModDirectory() + public string? PenumbraModDirectory; + + public string? GetPenumbraModDirectory() { if (!CheckPenumbraApi()) return null; return _penumbraResolveModDir!.Invoke().ToLowerInvariant(); @@ -495,6 +499,11 @@ public class IpcManager : MediatorSubscriberBase, IDisposable }); } + public (string[] forward, string[][] reverse) PenumbraResolvePaths(string[] forward, string[] reverse) + { + return _penumbraResolvePaths.Invoke(forward, reverse); + } + private void RedrawEvent(IntPtr objectAddress, int objectTableIndex) { Mediator.Publish(new PenumbraRedrawMessage(objectAddress, objectTableIndex)); @@ -533,7 +542,7 @@ public class IpcManager : MediatorSubscriberBase, IDisposable if (gameObj is Character c) { string decodedPalette = Encoding.UTF8.GetString(Convert.FromBase64String(palette)); - + if (string.IsNullOrEmpty(decodedPalette)) { Logger.Verbose("PalettePlus removing for " + c.Address.ToString("X")); @@ -555,7 +564,7 @@ public class IpcManager : MediatorSubscriberBase, IDisposable if (string.IsNullOrEmpty(palette)) return string.Empty; return Convert.ToBase64String(Encoding.UTF8.GetBytes(palette)); } - + public void PalettePlusRemovePalette(IntPtr character) { if (!CheckPalettePlusApi()) return; diff --git a/MareSynchronos/Managers/NotificationService.cs b/MareSynchronos/Managers/NotificationService.cs index 79de09d..fe7eac3 100644 --- a/MareSynchronos/Managers/NotificationService.cs +++ b/MareSynchronos/Managers/NotificationService.cs @@ -97,7 +97,7 @@ public class NotificationService : MediatorSubscriberBase private void PrintErrorChat(string? message) { - SeStringBuilder se = new SeStringBuilder().AddText("[Mare Synchronos] ").AddUiForeground("Error: ", 534).AddItalicsOn().AddUiForeground(message ?? string.Empty, 534).AddUiForegroundOff().AddItalicsOff(); - _chatGui.Print(se.BuiltString); + SeStringBuilder se = new SeStringBuilder().AddText("[Mare Synchronos] Error: " + message); + _chatGui.PrintError(se.BuiltString); } } diff --git a/MareSynchronos/Managers/OnlinePlayerManager.cs b/MareSynchronos/Managers/OnlinePlayerManager.cs index a74b040..728cc50 100644 --- a/MareSynchronos/Managers/OnlinePlayerManager.cs +++ b/MareSynchronos/Managers/OnlinePlayerManager.cs @@ -11,18 +11,17 @@ public class OnlinePlayerManager : MediatorSubscriberBase, IDisposable { private readonly ApiController _apiController; private readonly DalamudUtil _dalamudUtil; - private readonly PlayerManager _playerManager; private readonly FileCacheManager _fileDbManager; private readonly PairManager _pairManager; + private CharacterData? _lastSentData; - public OnlinePlayerManager(ApiController apiController, DalamudUtil dalamudUtil, PlayerManager playerManager, + public OnlinePlayerManager(ApiController apiController, DalamudUtil dalamudUtil, FileCacheManager fileDbManager, PairManager pairManager, MareMediator mediator) : base(mediator) { Logger.Verbose("Creating " + nameof(OnlinePlayerManager)); _apiController = apiController; _dalamudUtil = dalamudUtil; - _playerManager = playerManager; _fileDbManager = fileDbManager; _pairManager = pairManager; @@ -30,6 +29,20 @@ public class OnlinePlayerManager : MediatorSubscriberBase, IDisposable Mediator.Subscribe(this, (_) => DalamudUtilOnLogIn()); Mediator.Subscribe(this, (_) => DalamudUtilOnLogOut()); Mediator.Subscribe(this, (_) => FrameworkOnUpdate()); + Mediator.Subscribe(this, (msg) => + { + var newData = ((CharacterDataCreatedMessage)msg).CharacterData.ToAPI(); + if (_lastSentData == null || _lastSentData != null && !string.Equals(newData.DataHash.Value, _lastSentData.DataHash.Value, StringComparison.Ordinal)) + { + Logger.Debug("Pushing data for visible players"); + _lastSentData = newData; + PushCharacterData(_pairManager.VisibleUsers); + } + else + { + Logger.Debug("Not sending data for " + newData.DataHash.Value); + } + }); } private void PlayerManagerOnPlayerHasChanged(PlayerChangedMessage msg) @@ -78,11 +91,11 @@ public class OnlinePlayerManager : MediatorSubscriberBase, IDisposable private void PushCharacterData(List visiblePlayers) { - if (visiblePlayers.Any() && _playerManager.LastCreatedCharacterData != null) + if (visiblePlayers.Any() && _lastSentData != null) { Task.Run(async () => { - await _apiController.PushCharacterData(_playerManager.LastCreatedCharacterData, visiblePlayers).ConfigureAwait(false); + await _apiController.PushCharacterData(_lastSentData, visiblePlayers).ConfigureAwait(false); }); } } diff --git a/MareSynchronos/Managers/PairManager.cs b/MareSynchronos/Managers/PairManager.cs index 717f149..3f996c3 100644 --- a/MareSynchronos/Managers/PairManager.cs +++ b/MareSynchronos/Managers/PairManager.cs @@ -1,5 +1,4 @@ using Dalamud.Game.ClientState.Objects.SubKinds; -using Dalamud.Interface; using Dalamud.Interface.Internal.Notifications; using Dalamud.Utility; using MareSynchronos.API.Data; diff --git a/MareSynchronos/Managers/PlayerManager.cs b/MareSynchronos/Managers/PlayerManager.cs deleted file mode 100644 index 5793636..0000000 --- a/MareSynchronos/Managers/PlayerManager.cs +++ /dev/null @@ -1,279 +0,0 @@ -using MareSynchronos.Factories; -using MareSynchronos.Utils; -using MareSynchronos.WebAPI; -using FFXIVClientStructs.FFXIV.Client.Game.Character; -using MareSynchronos.Models; -using MareSynchronos.API.Data.Enum; -using MareSynchronos.Mediator; -#if DEBUG -#endif - -namespace MareSynchronos.Managers; - -public class PlayerManager : MediatorSubscriberBase, IDisposable -{ - private readonly ApiController _apiController; - private readonly CharacterDataFactory _characterDataFactory; - private readonly DalamudUtil _dalamudUtil; - private readonly IpcManager _ipcManager; - public API.Data.CharacterData? LastCreatedCharacterData { get; private set; } - public Models.CharacterData PermanentDataCache { get; private set; } = new(); - private readonly Dictionary> _objectKindsToUpdate = new(); - - private CancellationTokenSource? _playerChangedCts = new(); - private CancellationTokenSource _transientUpdateCts = new(); - - private readonly List _playerRelatedObjects = new(); - - public unsafe PlayerManager(ApiController apiController, IpcManager ipcManager, - CharacterDataFactory characterDataFactory, DalamudUtil dalamudUtil, - MareMediator mediator) : base(mediator) - { - Logger.Verbose("Creating " + nameof(PlayerManager)); - - _apiController = apiController; - _ipcManager = ipcManager; - _characterDataFactory = characterDataFactory; - _dalamudUtil = dalamudUtil; - - Mediator.Subscribe(this, (msg) => CustomizePlusChanged((CustomizePlusMessage)msg)); - Mediator.Subscribe(this, (msg) => HeelsOffsetChanged((HeelsOffsetMessage)msg)); - Mediator.Subscribe(this, (msg) => PalettePlusChanged((PalettePlusMessage)msg)); - Mediator.Subscribe(this, (_) => ApiControllerOnConnected()); - Mediator.Subscribe(this, (_) => ApiController_Disconnected()); - Mediator.Subscribe(this, (_) => DalamudUtilOnDelayedFrameworkUpdate()); - Mediator.Subscribe(this, (_) => DalamudUtilOnFrameworkUpdate()); - Mediator.Subscribe(this, (msg) => HandleTransientResourceLoad((TransientResourceChangedMessage)msg)); - - Logger.Debug("Watching Player, ApiController is Connected: " + _apiController.IsConnected); - if (_apiController.IsConnected) - { - ApiControllerOnConnected(); - } - - _playerRelatedObjects = new List() - { - new(ObjectKind.Player, IntPtr.Zero, IntPtr.Zero, () => _dalamudUtil.PlayerPointer), - new(ObjectKind.MinionOrMount, IntPtr.Zero, IntPtr.Zero, () => (IntPtr)((Character*)_dalamudUtil.PlayerPointer)->CompanionObject), - new(ObjectKind.Pet, IntPtr.Zero, IntPtr.Zero, () => _dalamudUtil.GetPet()), - new(ObjectKind.Companion, IntPtr.Zero, IntPtr.Zero, () => _dalamudUtil.GetCompanion()), - }; - } - - private void DalamudUtilOnFrameworkUpdate() - { - Mediator.Publish(new PlayerRelatedObjectPointerUpdateMessage(_playerRelatedObjects.Select(f => f.CurrentAddress).ToArray())); - } - - public void HandleTransientResourceLoad(TransientResourceChangedMessage msg) - { - foreach (var obj in _playerRelatedObjects) - { - if (obj.Address == msg.Address && !obj.HasUnprocessedUpdate) - { - _transientUpdateCts.Cancel(); - _transientUpdateCts = new CancellationTokenSource(); - var token = _transientUpdateCts.Token; - Task.Run(async () => - { - Logger.Debug("Delaying transient resource load update"); - await Task.Delay(750, token).ConfigureAwait(false); - if (obj.HasUnprocessedUpdate || token.IsCancellationRequested) return; - Logger.Debug("Firing transient resource load update"); - obj.HasTransientsUpdate = true; - }, token); - - return; - } - } - } - - private void HeelsOffsetChanged(HeelsOffsetMessage change) - { - var player = _playerRelatedObjects.First(f => f.ObjectKind == ObjectKind.Player); - if (LastCreatedCharacterData != null && LastCreatedCharacterData.HeelsOffset != change.Offset && !player.IsProcessing) - { - Logger.Debug("Heels offset changed to " + change.Offset); - player.HasTransientsUpdate = true; - } - } - - private void CustomizePlusChanged(CustomizePlusMessage msg) - { - var change = msg.Data ?? string.Empty; - var player = _playerRelatedObjects.First(f => f.ObjectKind == ObjectKind.Player); - if (LastCreatedCharacterData != null && !string.Equals(LastCreatedCharacterData.CustomizePlusData, change, StringComparison.Ordinal) && !player.IsProcessing) - { - Logger.Debug("CustomizePlus data changed to " + change); - player.HasTransientsUpdate = true; - } - } - - private void PalettePlusChanged(PalettePlusMessage msg) - { - var change = msg.Data ?? string.Empty; - var player = _playerRelatedObjects.First(f => f.ObjectKind == ObjectKind.Player); - if (LastCreatedCharacterData != null && !string.Equals(LastCreatedCharacterData.PalettePlusData, change, StringComparison.Ordinal) && !player.IsProcessing) - { - Logger.Debug("PalettePlus data changed to " + change); - player.HasTransientsUpdate = true; - } - } - - public override void Dispose() - { - base.Dispose(); - - _playerChangedCts?.Cancel(); - } - - private unsafe void DalamudUtilOnDelayedFrameworkUpdate() - { - if (!_dalamudUtil.IsPlayerPresent || !_ipcManager.Initialized) return; - - _playerRelatedObjects.ForEach(k => k.CheckAndUpdateObject()); - if (_playerRelatedObjects.Any(c => (c.HasUnprocessedUpdate || c.HasTransientsUpdate) && !c.IsProcessing)) - { - OnPlayerOrAttachedObjectsChanged(); - } - } - - private void ApiControllerOnConnected() - { - Logger.Debug("ApiController Connected"); - - Mediator.Subscribe(this, (msg) => IpcManager_PenumbraRedrawEvent((PenumbraRedrawMessage)msg)); - } - - private void ApiController_Disconnected() - { - Logger.Debug(nameof(ApiController_Disconnected)); - - Mediator.Unsubscribe(this); - } - - private async Task CreateFullCharacterCacheDto(CancellationToken token) - { - foreach (var unprocessedObject in _playerRelatedObjects.Where(c => c.HasUnprocessedUpdate || c.HasTransientsUpdate).ToList()) - { - Logger.Verbose("Building Cache for " + unprocessedObject.ObjectKind); - PermanentDataCache = _characterDataFactory.BuildCharacterData(PermanentDataCache, unprocessedObject, token); - if (!token.IsCancellationRequested) - { - unprocessedObject.HasUnprocessedUpdate = false; - unprocessedObject.IsProcessing = false; - unprocessedObject.HasTransientsUpdate = false; - } - token.ThrowIfCancellationRequested(); - } - - int timeOut = 10000; - while (!PermanentDataCache.IsReady && !token.IsCancellationRequested && timeOut >= 0) - { - Logger.Verbose("Waiting until cache is ready (Timeout: " + TimeSpan.FromMilliseconds(timeOut) + ")"); - await Task.Delay(50, token).ConfigureAwait(false); - timeOut -= 50; - } - - if (token.IsCancellationRequested || timeOut <= 0) return null; - - Logger.Verbose("Cache creation complete"); - - var cache = PermanentDataCache.ToAPI(); - //Logger.Verbose(JsonConvert.SerializeObject(cache, Formatting.Indented)); - return cache; - } - - private void IpcManager_PenumbraRedrawEvent(PenumbraRedrawMessage msg) - { - Logger.Verbose("RedrawEvent for addr " + msg.Address); - - foreach (var item in _playerRelatedObjects) - { - if (msg.Address == item.Address) - { - Logger.Debug("Penumbra redraw Event for " + item.ObjectKind); - item.HasUnprocessedUpdate = true; - } - } - - if (_playerRelatedObjects.Any(c => (c.HasUnprocessedUpdate || c.HasTransientsUpdate) && (!c.IsProcessing || (c.IsProcessing && c.DoNotSendUpdate)))) - { - OnPlayerOrAttachedObjectsChanged(); - } - } - - private void OnPlayerOrAttachedObjectsChanged() - { - var unprocessedObjects = _playerRelatedObjects.Where(c => c.HasUnprocessedUpdate || c.HasTransientsUpdate).ToList(); - foreach (var unprocessedObject in unprocessedObjects) - { - unprocessedObject.IsProcessing = true; - } - Logger.Debug("Object(s) changed: " + string.Join(", ", unprocessedObjects.Select(c => c.ObjectKind))); - bool doNotSendUpdate = unprocessedObjects.All(c => c.DoNotSendUpdate); - unprocessedObjects.ForEach(p => p.DoNotSendUpdate = false); - _playerChangedCts?.Cancel(); - _playerChangedCts = new CancellationTokenSource(); - var token = _playerChangedCts.Token; - - // fix for redraw from anamnesis - while ((!_dalamudUtil.IsPlayerPresent || string.Equals(_dalamudUtil.PlayerName, "--", StringComparison.Ordinal)) && !token.IsCancellationRequested) - { - Logger.Debug("Waiting Until Player is Present"); - Thread.Sleep(100); - } - - if (token.IsCancellationRequested) - { - Logger.Debug("Cancelled"); - return; - } - - if (!_ipcManager.Initialized) - { - Logger.Warn("Penumbra not active, doing nothing."); - return; - } - - Task.Run(async () => - { - API.Data.CharacterData? cacheData = null; - try - { - Mediator.Publish(new HaltScanMessage("Character creation")); - foreach (var item in unprocessedObjects) - { - _dalamudUtil.WaitWhileCharacterIsDrawing("self " + item.ObjectKind.ToString(), item.Address, item.ObjectKind == ObjectKind.MinionOrMount ? 1000 : 10000, token); - } - - cacheData = (await CreateFullCharacterCacheDto(token).ConfigureAwait(false)); - } - catch { } - finally - { - Mediator.Publish(new ResumeScanMessage("Character creation")); - } - if (cacheData == null || token.IsCancellationRequested) return; - -#if DEBUG - //var json = JsonConvert.SerializeObject(cacheDto, Formatting.Indented); - //Logger.Verbose(json); -#endif - - if (string.Equals(LastCreatedCharacterData?.DataHash.Value ?? string.Empty, cacheData.DataHash.Value, StringComparison.Ordinal)) - { - Logger.Debug("Not sending data, already sent"); - return; - } - - LastCreatedCharacterData = cacheData; - - if (_apiController.IsConnected && !token.IsCancellationRequested && !doNotSendUpdate) - { - Logger.Verbose("Invoking PlayerHasChanged"); - Mediator.Publish(new PlayerChangedMessage(cacheData)); - } - }, token); - } -} diff --git a/MareSynchronos/Managers/TransientResourceManager.cs b/MareSynchronos/Managers/TransientResourceManager.cs index 0522551..21b942d 100644 --- a/MareSynchronos/Managers/TransientResourceManager.cs +++ b/MareSynchronos/Managers/TransientResourceManager.cs @@ -1,8 +1,6 @@ using MareSynchronos.API.Data.Enum; -using MareSynchronos.Factories; using MareSynchronos.MareConfiguration; using MareSynchronos.Mediator; -using MareSynchronos.Models; using MareSynchronos.Utils; using System.Collections.Concurrent; @@ -16,13 +14,11 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable public IntPtr[] PlayerRelatedPointers = Array.Empty(); private readonly string[] _fileTypesToHandle = new[] { "tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp", "shpk" }; - [Obsolete] - private string PersistentDataCache => Path.Combine(_configurationService.ConfigurationDirectory, "PersistentTransientData.lst"); private string PlayerPersistentDataKey => _dalamudUtil.PlayerName + "_" + _dalamudUtil.WorldId; private ConcurrentDictionary> TransientResources { get; } = new(); - private ConcurrentDictionary> SemiTransientResources { get; } = new(); - public TransientResourceManager(ConfigurationService configurationService, DalamudUtil dalamudUtil, FileReplacementFactory fileReplacementFactory, MareMediator mediator) : base(mediator) + private ConcurrentDictionary> SemiTransientResources { get; } = new(); + public TransientResourceManager(ConfigurationService configurationService, DalamudUtil dalamudUtil, MareMediator mediator) : base(mediator) { _configurationService = configurationService; _dalamudUtil = dalamudUtil; @@ -32,16 +28,8 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable Mediator.Subscribe(this, (_) => DalamudUtil_FrameworkUpdate()); Mediator.Subscribe(this, (_) => DalamudUtil_ClassJobChanged()); Mediator.Subscribe(this, (msg) => PlayerRelatedPointers = ((PlayerRelatedObjectPointerUpdateMessage)msg).RelatedObjects); - // migrate obsolete data to new format - if (File.Exists(PersistentDataCache)) - { - var persistentEntities = File.ReadAllLines(PersistentDataCache).ToHashSet(StringComparer.OrdinalIgnoreCase); - _configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey] = persistentEntities; - _configurationService.Save(); - File.Delete(PersistentDataCache); - } - SemiTransientResources.TryAdd(ObjectKind.Player, new HashSet()); + SemiTransientResources.TryAdd(ObjectKind.Player, new HashSet(StringComparer.Ordinal)); if (_configurationService.Current.PlayerPersistentTransientCache.TryGetValue(PlayerPersistentDataKey, out var linesInConfig)) { int restored = 0; @@ -49,14 +37,9 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable { try { - var fileReplacement = fileReplacementFactory.Create(); - fileReplacement.ResolvePath(file); - if (fileReplacement.HasFileReplacement) - { - Logger.Debug("Loaded persistent transient resource " + file); - SemiTransientResources[ObjectKind.Player].Add(fileReplacement); - restored++; - } + Logger.Debug("Loaded persistent transient resource " + file); + SemiTransientResources[ObjectKind.Player].Add(file); + restored++; } catch (Exception ex) { @@ -70,20 +53,12 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable private void Manager_PenumbraModSettingChanged() { - bool successfulValidation = true; Task.Run(() => { Logger.Debug("Penumbra Mod Settings changed, verifying SemiTransientResources"); foreach (var item in SemiTransientResources) { - item.Value.RemoveWhere(p => - { - var verified = p.Verify(); - successfulValidation &= verified; - return !verified; - }); - if (!successfulValidation) - Mediator.Publish(new TransientResourceChangedMessage(_dalamudUtil.PlayerPointer)); + Mediator.Publish(new TransientResourceChangedMessage(_dalamudUtil.PlayerPointer)); } }); } @@ -126,19 +101,19 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable return new List(); } - public List GetSemiTransientResources(ObjectKind objectKind) + public HashSet GetSemiTransientResources(ObjectKind objectKind) { if (SemiTransientResources.TryGetValue(objectKind, out var result)) { - return result.ToList(); + return result; } - return new List(); + return new HashSet(); } private void Manager_PenumbraResourceLoadEvent(PenumbraResourceLoadMessage msg) { - var gamePath = msg.GamePath; + var gamePath = msg.GamePath.ToLowerInvariant(); var gameObject = msg.GameObject; var filePath = msg.FilePath; if (!_fileTypesToHandle.Any(type => gamePath.EndsWith(type, StringComparison.OrdinalIgnoreCase))) @@ -166,14 +141,9 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable if (string.Equals(filePath, replacedGamePath, StringComparison.OrdinalIgnoreCase)) return; if (TransientResources[gameObject].Contains(replacedGamePath) || - SemiTransientResources.Any(r => r.Value.Any(f => - string.Equals(f.GamePaths.First(), replacedGamePath, StringComparison.OrdinalIgnoreCase) - && string.Equals(f.ResolvedPath, filePath, StringComparison.OrdinalIgnoreCase)) - )) + SemiTransientResources.Any(r => r.Value.Any(f => string.Equals(f, gamePath, StringComparison.OrdinalIgnoreCase)))) { Logger.Verbose("Not adding " + replacedGamePath + ":" + filePath); - Logger.Verbose("SemiTransientAny: " + SemiTransientResources.Any(r => r.Value.Any(f => string.Equals(f.GamePaths.First(), replacedGamePath, StringComparison.OrdinalIgnoreCase) - && string.Equals(f.ResolvedPath, filePath, StringComparison.OrdinalIgnoreCase))).ToString() + ", TransientAny: " + TransientResources[gameObject].Contains(replacedGamePath)); } else { @@ -183,19 +153,11 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable } } - public void RemoveTransientResource(IntPtr gameObject, FileReplacement fileReplacement) - { - if (TransientResources.ContainsKey(gameObject)) - { - TransientResources[gameObject].RemoveWhere(f => fileReplacement.GamePaths.Any(g => string.Equals(g, f, StringComparison.OrdinalIgnoreCase))); - } - } - - public void PersistTransientResources(IntPtr gameObject, ObjectKind objectKind, Func createFileReplacement) + public void PersistTransientResources(IntPtr gameObject, ObjectKind objectKind) { if (!SemiTransientResources.ContainsKey(objectKind)) { - SemiTransientResources[objectKind] = new HashSet(); + SemiTransientResources[objectKind] = new HashSet(StringComparer.Ordinal); } if (!TransientResources.TryGetValue(gameObject, out var resources)) @@ -203,47 +165,16 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable return; } - SemiTransientResources[objectKind].RemoveWhere(p => !p.Verify()); - var transientResources = resources.ToList(); Logger.Debug("Persisting " + transientResources.Count + " transient resources"); foreach (var gamePath in transientResources) { - var existingResource = SemiTransientResources[objectKind].Any(f => string.Equals(f.GamePaths.First(), gamePath, StringComparison.OrdinalIgnoreCase)); - if (existingResource) - { - Logger.Debug("Semi Transient resource replaced: " + gamePath); - SemiTransientResources[objectKind].RemoveWhere(f => string.Equals(f.GamePaths.First(), gamePath, StringComparison.OrdinalIgnoreCase)); - } - - try - { - var fileReplacement = createFileReplacement(gamePath.ToLowerInvariant(), arg2: true); - if (!fileReplacement.HasFileReplacement) - fileReplacement = createFileReplacement(gamePath.ToLowerInvariant(), arg2: false); - if (fileReplacement.HasFileReplacement) - { - Logger.Debug("Persisting " + gamePath.ToLowerInvariant()); - if (SemiTransientResources[objectKind].Add(fileReplacement)) - { - Logger.Debug("Added " + fileReplacement); - } - else - { - Logger.Debug("Not added " + fileReplacement); - } - } - } - catch (Exception ex) - { - Logger.Warn("Issue during transient file persistence", ex); - } + SemiTransientResources[objectKind].Add(gamePath); } if (objectKind == ObjectKind.Player && SemiTransientResources.TryGetValue(ObjectKind.Player, out var fileReplacements)) { - _configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey] - = fileReplacements.SelectMany(p => p.GamePaths).Distinct(StringComparer.OrdinalIgnoreCase).ToHashSet(StringComparer.OrdinalIgnoreCase); + _configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey] = fileReplacements; _configurationService.Save(); } TransientResources[gameObject].Clear(); @@ -256,22 +187,26 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable SemiTransientResources.Clear(); if (SemiTransientResources.ContainsKey(ObjectKind.Player)) { - _configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey] - = SemiTransientResources[ObjectKind.Player].SelectMany(p => p.GamePaths).Distinct(StringComparer.OrdinalIgnoreCase).ToHashSet(StringComparer.OrdinalIgnoreCase); + _configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey] = SemiTransientResources[ObjectKind.Player]; _configurationService.Save(); } } - internal void AddSemiTransientResource(ObjectKind objectKind, FileReplacement item) + internal void AddSemiTransientResource(ObjectKind objectKind, string item) { if (!SemiTransientResources.ContainsKey(objectKind)) { - SemiTransientResources[objectKind] = new HashSet(); + SemiTransientResources[objectKind] = new HashSet(StringComparer.Ordinal); } - if (!SemiTransientResources[objectKind].Any(f => string.Equals(f.ResolvedPath, item.ResolvedPath, StringComparison.OrdinalIgnoreCase))) + SemiTransientResources[objectKind].Add(item.ToLowerInvariant()); + } + + internal void ClearTransientPaths(IntPtr ptr, List list) + { + if (TransientResources.TryGetValue(ptr, out var set)) { - SemiTransientResources[objectKind].Add(item); + set.RemoveWhere(p => list.Contains(p, StringComparer.OrdinalIgnoreCase)); } } } diff --git a/MareSynchronos/MarePlugin.cs b/MareSynchronos/MarePlugin.cs index 2d3316b..c6596ac 100644 --- a/MareSynchronos/MarePlugin.cs +++ b/MareSynchronos/MarePlugin.cs @@ -94,8 +94,8 @@ public class MarePlugin : MediatorSubscriberBase, IDisposable _runtimeServiceScope?.Dispose(); _runtimeServiceScope = _serviceProvider.CreateScope(); + _runtimeServiceScope.ServiceProvider.GetRequiredService(); _runtimeServiceScope.ServiceProvider.GetRequiredService(); - _runtimeServiceScope.ServiceProvider.GetRequiredService(); _runtimeServiceScope.ServiceProvider.GetRequiredService(); } catch (Exception ex) diff --git a/MareSynchronos/MareSynchronos.csproj b/MareSynchronos/MareSynchronos.csproj index 65d35c4..c7b0e61 100644 --- a/MareSynchronos/MareSynchronos.csproj +++ b/MareSynchronos/MareSynchronos.csproj @@ -3,7 +3,7 @@ - 0.7.13 + 0.7.14 https://github.com/Penumbra-Sync/client @@ -33,7 +33,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/MareSynchronos/Mediator/Messages.cs b/MareSynchronos/Mediator/Messages.cs index ef0d437..45094d9 100644 --- a/MareSynchronos/Mediator/Messages.cs +++ b/MareSynchronos/Mediator/Messages.cs @@ -1,4 +1,5 @@ using Dalamud.Interface.Internal.Notifications; +using MareSynchronos.Models; namespace MareSynchronos.Mediator; @@ -35,4 +36,7 @@ public record ResumeScanMessage(string Source) : IMessage; public record NotificationMessage (string Title, string Message, NotificationType Type, uint TimeShownOnScreen = 3000) : IMessage; +public record CreateCacheForObjectMessage(GameObjectHandler ObjectToCreateFor) : IMessage; +public record CharacterDataCreatedMessage(CharacterData CharacterData) : IMessage; + #pragma warning restore MA0048 // File name must match type name \ No newline at end of file diff --git a/MareSynchronos/Models/CharacterData.cs b/MareSynchronos/Models/CharacterData.cs index 2e765ec..8f6c948 100644 --- a/MareSynchronos/Models/CharacterData.cs +++ b/MareSynchronos/Models/CharacterData.cs @@ -1,6 +1,5 @@ using Newtonsoft.Json; using System.Text; -using MareSynchronos.Utils; using MareSynchronos.API.Data.Enum; using MareSynchronos.API.Data; @@ -10,7 +9,7 @@ namespace MareSynchronos.Models; public class CharacterData { [JsonProperty] - public Dictionary> FileReplacements { get; set; } = new(); + public Dictionary> FileReplacements { get; set; } = new(); [JsonProperty] public Dictionary GlamourerString { get; set; } = new(); @@ -33,7 +32,7 @@ public class CharacterData { if (!fileReplacement.HasFileReplacement) return; - if (!FileReplacements.ContainsKey(objectKind)) FileReplacements.Add(objectKind, new List()); + if (!FileReplacements.ContainsKey(objectKind)) FileReplacements.Add(objectKind, new HashSet()); var existingReplacement = FileReplacements[objectKind].SingleOrDefault(f => string.Equals(f.ResolvedPath, fileReplacement.ResolvedPath, StringComparison.OrdinalIgnoreCase)); if (existingReplacement != null) @@ -57,16 +56,9 @@ public class CharacterData }; }).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); } diff --git a/MareSynchronos/Models/FileReplacement.cs b/MareSynchronos/Models/FileReplacement.cs index 9cfd601..cb6d97f 100644 --- a/MareSynchronos/Models/FileReplacement.cs +++ b/MareSynchronos/Models/FileReplacement.cs @@ -1,79 +1,31 @@ -using System.Text; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using MareSynchronos.FileCache; -using MareSynchronos.Managers; -using MareSynchronos.Utils; using MareSynchronos.API.Data; namespace MareSynchronos.Models; public class FileReplacement { - private readonly FileCacheManager _fileDbManager; - private readonly IpcManager _ipcManager; - - public FileReplacement(FileCacheManager fileDbManager, IpcManager ipcManager) + public FileReplacement(List gamePaths, string filePath, FileCacheManager fileDbManager) { - _fileDbManager = fileDbManager; - _ipcManager = ipcManager; + GamePaths = gamePaths.Select(g => g.Replace('\\', '/')).ToList(); + ResolvedPath = filePath.Replace('\\', '/'); + HashLazy = new(() => !IsFileSwap ? fileDbManager.GetFileCacheByPath(ResolvedPath)?.Hash ?? string.Empty : string.Empty); } public bool Computed => IsFileSwap || !HasFileReplacement || !string.IsNullOrEmpty(Hash); - public List GamePaths { get; set; } = new(); + public List GamePaths { get; init; } = new(); public bool HasFileReplacement => GamePaths.Count >= 1 && GamePaths.Any(p => !string.Equals(p, ResolvedPath, StringComparison.Ordinal)); public bool IsFileSwap => !Regex.IsMatch(ResolvedPath, @"^[a-zA-Z]:(/|\\)", RegexOptions.ECMAScript) && !string.Equals(GamePaths[0], ResolvedPath, StringComparison.Ordinal); - public string Hash { get; private set; } = string.Empty; + public string Hash => HashLazy.Value; - public string ResolvedPath { get; set; } = string.Empty; + private Lazy HashLazy; - private void SetResolvedPath(string path) - { - ResolvedPath = path.ToLowerInvariant().Replace('\\', '/'); - if (!HasFileReplacement || IsFileSwap) return; - - _ = Task.Run(() => - { - try - { - var cache = _fileDbManager.GetFileCacheByPath(ResolvedPath)!; - Hash = cache.Hash; - } - catch (Exception ex) - { - Logger.Warn("Could not set Hash for " + ResolvedPath + ", resetting to original", ex); - ResolvedPath = GamePaths[0]; - } - }); - } - - public bool Verify() - { - if (!IsFileSwap) - { - var cache = _fileDbManager.GetFileCacheByPath(ResolvedPath); - if (cache == null) - { - Logger.Warn("Replacement Failed verification: " + GamePaths[0]); - return false; - } - Hash = cache.Hash; - return true; - } - - ResolvePath(GamePaths[0]); - - var success = IsFileSwap; - if (!success) - { - Logger.Warn("FileSwap Failed verification: " + GamePaths[0]); - } - - return success; - } + public string ResolvedPath { get; init; } = string.Empty; public FileReplacementData ToFileReplacementDto() { @@ -87,20 +39,6 @@ public class FileReplacement public override string ToString() { - StringBuilder builder = new(); - builder.AppendLine($"Modded: {HasFileReplacement} - {string.Join(",", GamePaths)} => {ResolvedPath}"); - return builder.ToString(); - } - - internal void ReverseResolvePath(string path) - { - GamePaths = _ipcManager.PenumbraReverseResolvePlayer(path).ToList(); - SetResolvedPath(path); - } - - internal void ResolvePath(string path) - { - GamePaths = new List { path }; - SetResolvedPath(_ipcManager.PenumbraResolvePath(path)); + return $"Modded: {HasFileReplacement} - {string.Join(",", GamePaths)} => {ResolvedPath}"; } } diff --git a/MareSynchronos/Models/PlayerRelatedObject.cs b/MareSynchronos/Models/GameObjectHandler.cs similarity index 74% rename from MareSynchronos/Models/PlayerRelatedObject.cs rename to MareSynchronos/Models/GameObjectHandler.cs index 398e92d..5e1c6b0 100644 --- a/MareSynchronos/Models/PlayerRelatedObject.cs +++ b/MareSynchronos/Models/GameObjectHandler.cs @@ -3,12 +3,15 @@ using System.Runtime.InteropServices; using MareSynchronos.Utils; using Penumbra.String; using MareSynchronos.API.Data.Enum; +using MareSynchronos.Mediator; namespace MareSynchronos.Models; -public class PlayerRelatedObject +public class GameObjectHandler : MediatorSubscriberBase { + private readonly MareMediator _mediator; private readonly Func getAddress; + private readonly bool _sendUpdates; public unsafe Character* Character => (Character*)Address; @@ -31,26 +34,31 @@ public class PlayerRelatedObject } } - public PlayerRelatedObject(ObjectKind objectKind, IntPtr address, IntPtr drawObjectAddress, Func getAddress) + public GameObjectHandler(MareMediator mediator, ObjectKind objectKind, Func getAddress, bool sendUpdates = true) : base(mediator) { + _mediator = mediator; ObjectKind = objectKind; - Address = address; - DrawObjectAddress = drawObjectAddress; this.getAddress = getAddress; + _sendUpdates = sendUpdates; _name = string.Empty; + + Mediator.Subscribe(this, (msg) => + { + var actualMsg = (TransientResourceChangedMessage)msg; + if (actualMsg.Address != Address || !sendUpdates) return; + Mediator.Publish(new CreateCacheForObjectMessage(this)); + }); + + Mediator.Subscribe(this, (_) => CheckAndUpdateObject()); } public byte[] EquipSlotData { get; set; } = new byte[40]; public byte[] CustomizeData { get; set; } = new byte[26]; public byte? HatState { get; set; } public byte? VisorWeaponState { get; set; } + private bool _doNotSendUpdate; - public bool HasTransientsUpdate { get; set; } = false; - public bool HasUnprocessedUpdate { get; set; } = false; - public bool DoNotSendUpdate { get; set; } = false; - public bool IsProcessing { get; set; } = false; - - public unsafe void CheckAndUpdateObject() + public unsafe bool CheckAndUpdateObject() { var curPtr = CurrentAddress; if (curPtr != IntPtr.Zero) @@ -68,7 +76,10 @@ public class PlayerRelatedObject Address = curPtr; DrawObjectAddress = (IntPtr)chara->GameObject.DrawObject; - HasUnprocessedUpdate = true; + if (_sendUpdates && !_doNotSendUpdate) + Mediator.Publish(new CreateCacheForObjectMessage(this)); + + return true; } } else if (Address != IntPtr.Zero || DrawObjectAddress != IntPtr.Zero) @@ -77,12 +88,14 @@ public class PlayerRelatedObject DrawObjectAddress = IntPtr.Zero; Logger.Verbose(ObjectKind + " Changed: " + _name + ", now: " + Address + ", " + DrawObjectAddress); } + + return false; } private unsafe bool CompareAndUpdateByteData(byte* equipSlotData, byte* customizeData) { bool hasChanges = false; - DoNotSendUpdate = false; + _doNotSendUpdate = false; for (int i = 0; i < EquipSlotData.Length; i++) { var data = Marshal.ReadByte((IntPtr)equipSlotData, i); @@ -107,10 +120,10 @@ public class PlayerRelatedObject var newWeaponOrVisorState = Marshal.ReadByte((IntPtr)customizeData + 31, 0); if (newHatState != HatState) { - if (HatState != null && !hasChanges && !HasUnprocessedUpdate) + if (HatState != null && !hasChanges) { Logger.Debug("Not Sending Update, only Hat changed"); - DoNotSendUpdate = true; + _doNotSendUpdate = true; } HatState = newHatState; hasChanges = true; @@ -120,10 +133,10 @@ public class PlayerRelatedObject if (newWeaponOrVisorState != VisorWeaponState) { - if (VisorWeaponState != null && !hasChanges && !HasUnprocessedUpdate) + if (VisorWeaponState != null && !hasChanges) { Logger.Debug("Not Sending Update, only Visor/Weapon changed"); - DoNotSendUpdate = true; + _doNotSendUpdate = true; } VisorWeaponState = newWeaponOrVisorState; hasChanges = true; diff --git a/MareSynchronos/Plugin.cs b/MareSynchronos/Plugin.cs index 11324f0..63782be 100644 --- a/MareSynchronos/Plugin.cs +++ b/MareSynchronos/Plugin.cs @@ -59,7 +59,6 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); - collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); @@ -70,9 +69,9 @@ public sealed class Plugin : IDalamudPlugin collection.AddSingleton(); collection.AddSingleton(); + collection.AddScoped(); collection.AddScoped(); collection.AddScoped(); - collection.AddScoped(); collection.AddScoped(); var serviceProvider = collection.BuildServiceProvider(new ServiceProviderOptions() { ValidateOnBuild = true, ValidateScopes = true }); diff --git a/MareSynchronos/UI/SettingsUi.cs b/MareSynchronos/UI/SettingsUi.cs index 2fb6e7f..1cf5b0a 100644 --- a/MareSynchronos/UI/SettingsUi.cs +++ b/MareSynchronos/UI/SettingsUi.cs @@ -57,7 +57,7 @@ public class SettingsUi : WindowMediatorSubscriberBase, IDisposable Mediator.Subscribe(this, (_) => IsOpen = false); Mediator.Subscribe(this, (_) => UiShared_GposeStart()); Mediator.Subscribe(this, (_) => UiShared_GposeEnd()); - Mediator.Subscribe(this, (msg) => LastCreatedCharacterData = ((PlayerChangedMessage)msg).Data); + Mediator.Subscribe(this, (msg) => LastCreatedCharacterData = ((CharacterDataCreatedMessage)msg).CharacterData.ToAPI()); windowSystem.AddWindow(this); } diff --git a/MareSynchronos/UI/UIShared.cs b/MareSynchronos/UI/UIShared.cs index d6a11c6..cc9e375 100644 --- a/MareSynchronos/UI/UIShared.cs +++ b/MareSynchronos/UI/UIShared.cs @@ -38,7 +38,7 @@ public partial class UiShared : IDisposable public string PlayerName => _dalamudUtil.PlayerName; public uint WorldId => _dalamudUtil.WorldId; public Dictionary WorldData => _dalamudUtil.WorldData.Value; - public bool HasValidPenumbraModPath => !(_ipcManager.PenumbraModDirectory() ?? string.Empty).IsNullOrEmpty() && Directory.Exists(_ipcManager.PenumbraModDirectory()); + public bool HasValidPenumbraModPath => !(_ipcManager.PenumbraModDirectory ?? string.Empty).IsNullOrEmpty() && Directory.Exists(_ipcManager.PenumbraModDirectory); public bool EditTrackerPosition { get; set; } public ImFontPtr UidFont { get; private set; } public bool UidFontBuilt { get; private set; } @@ -468,7 +468,7 @@ public partial class UiShared : IDisposable { if (!success) return; - _isPenumbraDirectory = string.Equals(path.ToLowerInvariant(), _ipcManager.PenumbraModDirectory()?.ToLowerInvariant(), StringComparison.Ordinal); + _isPenumbraDirectory = string.Equals(path.ToLowerInvariant(), _ipcManager.PenumbraModDirectory?.ToLowerInvariant(), StringComparison.Ordinal); _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 5752bd3..e690068 100644 --- a/MareSynchronos/Utils/DalamudUtil.cs +++ b/MareSynchronos/Utils/DalamudUtil.cs @@ -226,11 +226,13 @@ public class DalamudUtil : IDisposable { if (!_clientState.IsLoggedIn || characterAddress == IntPtr.Zero) return; + Logger.Verbose($"Starting wait for {name} to draw"); + var obj = (GameObject*)characterAddress; const int tick = 250; int curWaitTime = 0; // ReSharper disable once LoopVariableIsNeverChangedInsideLoop - while ((obj->RenderFlags & 0b100000000000) == 0b100000000000 && (!ct?.IsCancellationRequested ?? true) && curWaitTime < timeOut) // 0b100000000000 is "still rendering" or something + while ((obj->DrawObject == null || (obj->RenderFlags & 0b100000000000) == 0b100000000000) && (!ct?.IsCancellationRequested ?? true) && curWaitTime < timeOut) // 0b100000000000 is "still rendering" or something { Logger.Verbose($"Waiting for {name} to finish drawing"); curWaitTime += tick; diff --git a/MareSynchronos/Utils/FileReplacementComparer.cs b/MareSynchronos/Utils/FileReplacementComparer.cs new file mode 100644 index 0000000..a2be7dc --- /dev/null +++ b/MareSynchronos/Utils/FileReplacementComparer.cs @@ -0,0 +1,45 @@ +using MareSynchronos.Models; + +namespace MareSynchronos.Utils; + +public class FileReplacementComparer : IEqualityComparer +{ + public static FileReplacementComparer Instance => _instance; + private static FileReplacementComparer _instance = new(); + private FileReplacementComparer() { } + public bool Equals(FileReplacement? x, FileReplacement? y) + { + if (x == null || y == null) return false; + return x.ResolvedPath.Equals(y.ResolvedPath) && CompareLists(x.GamePaths, y.GamePaths); + } + + public int GetHashCode(FileReplacement obj) + { + return HashCode.Combine(obj.ResolvedPath.GetHashCode(StringComparison.OrdinalIgnoreCase), GetOrderIndependentHashCode(obj.GamePaths)); + } + + private static int GetOrderIndependentHashCode(IEnumerable source) + { + int hash = 0; + foreach (T element in source) + { + hash = unchecked(hash + + EqualityComparer.Default.GetHashCode(element)); + } + return hash; + } + + private bool CompareLists(List list1, List list2) + { + if (list1.Count != list2.Count) + return false; + + for (int i = 0; i < list1.Count; i++) + { + if (!string.Equals(list1[i], list2[i], StringComparison.OrdinalIgnoreCase)) + return false; + } + + return true; + } +} \ No newline at end of file