diff --git a/MareSynchronos/Factories/CharacterDataFactory.cs b/MareSynchronos/Factories/CharacterDataFactory.cs index a851720..84a0af2 100644 --- a/MareSynchronos/Factories/CharacterDataFactory.cs +++ b/MareSynchronos/Factories/CharacterDataFactory.cs @@ -1,5 +1,4 @@ using System.Diagnostics; -using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.System.Resource; @@ -36,7 +35,7 @@ public class CharacterDataFactory return ((Character*)playerPointer)->GameObject.DrawObject == null; } - public CharacterData BuildCharacterData(CharacterData previousData, GameObjectHandler playerRelatedObject, CancellationToken token) + public async Task BuildCharacterData(CharacterData previousData, GameObjectHandler playerRelatedObject, CancellationToken token) { if (!_ipcManager.Initialized) { @@ -75,9 +74,7 @@ public class CharacterDataFactory try { - _pathsToForwardResolve.Clear(); - _pathsToReverseResolve.Clear(); - return CreateCharacterData(previousData, playerRelatedObject, token); + return await CreateCharacterData(previousData, playerRelatedObject, token).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -94,7 +91,171 @@ public class CharacterDataFactory return previousData; } - private unsafe void AddReplacementsFromRenderModel(RenderModel* mdl) + private async Task CreateCharacterData(CharacterData previousData, GameObjectHandler playerRelatedObject, CancellationToken token) + { + var objectKind = playerRelatedObject.ObjectKind; + var charaPointer = playerRelatedObject.Address; + + Logger.Debug("Building character data for " + objectKind); + + if (!previousData.FileReplacements.ContainsKey(objectKind)) + { + previousData.FileReplacements[objectKind] = new(FileReplacementComparer.Instance); + } + else + { + previousData.FileReplacements[objectKind].Clear(); + } + + // wait until chara is not drawing and present so nothing spontaneously explodes + _dalamudUtil.WaitWhileCharacterIsDrawing(playerRelatedObject.ObjectKind.ToString(), playerRelatedObject.Address, 30000, ct: token); + var chara = _dalamudUtil.CreateGameObject(charaPointer)!; + while (!DalamudUtil.IsObjectPresent(chara)) + { + Logger.Verbose("Character is null but it shouldn't be, waiting"); + await Task.Delay(50).ConfigureAwait(false); + } + + Stopwatch st = Stopwatch.StartNew(); + + // gather up data from ipc + previousData.ManipulationString = await _dalamudUtil.RunOnFrameworkThread(_ipcManager.PenumbraGetMetaManipulations).ConfigureAwait(false); + previousData.GlamourerString[playerRelatedObject.ObjectKind] = await _dalamudUtil.RunOnFrameworkThread(() => _ipcManager.GlamourerGetCharacterCustomization(playerRelatedObject.Address)) + .ConfigureAwait(false); + previousData.HeelsOffset = await _dalamudUtil.RunOnFrameworkThread(_ipcManager.GetHeelsOffset).ConfigureAwait(false); + previousData.CustomizePlusScale = await _dalamudUtil.RunOnFrameworkThread(_ipcManager.GetCustomizePlusScale).ConfigureAwait(false); + previousData.PalettePlusPalette = await _dalamudUtil.RunOnFrameworkThread(_ipcManager.PalettePlusBuildPalette).ConfigureAwait(false); + + // gather static replacements from render model + var (forwardResolve, reverseResolve) = BuildDataFromModel(objectKind, charaPointer, token); + Dictionary> resolvedPaths = GetFileReplacementsFromPaths(forwardResolve, reverseResolve); + previousData.FileReplacements[objectKind] = + new HashSet(resolvedPaths.Select(c => new FileReplacement(c.Value, c.Key, _fileCacheManager)), FileReplacementComparer.Instance) + .Where(p => p.HasFileReplacement).ToHashSet(); + + Logger.Debug("== Static Replacements =="); + foreach (var replacement in previousData.FileReplacements[objectKind].Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase)) + { + Logger.Debug(replacement.ToString()); + } + + // if it's pet then it's summoner, if it's summoner we actually want to keep all filereplacements alive at all times + // or we get into redraw city for every change and nothing works properly + if (objectKind == ObjectKind.Pet) + { + foreach (var item in previousData.FileReplacements[objectKind].Where(i => i.HasFileReplacement).SelectMany(p => p.GamePaths)) + { + _transientResourceManager.AddSemiTransientResource(objectKind, item); + } + } + + Logger.Debug("Handling transient update for " + objectKind); + + // remove all potentially gathered paths from the transient resource manager that are resolved through static resolving + _transientResourceManager.ClearTransientPaths(charaPointer, previousData.FileReplacements[objectKind].SelectMany(c => c.GamePaths).ToList()); + + // get all remaining paths and resolve them + var transientPaths = ManageSemiTransientData(objectKind, charaPointer); + var resolvedTransientPaths = GetFileReplacementsFromPaths(transientPaths, new HashSet(StringComparer.Ordinal)); + + Logger.Debug("== Transient Replacements =="); + foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement(c.Value, c.Key, _fileCacheManager)).OrderBy(f => f.ResolvedPath, StringComparer.Ordinal)) + { + Logger.Debug(replacement.ToString()); + previousData.FileReplacements[objectKind].Add(replacement); + } + + // clean up all semi transient resources that don't have any file replacement (aka null resolve) + _transientResourceManager.CleanUpSemiTransientResources(objectKind, previousData.FileReplacements[objectKind].ToList()); + + // make sure we only return data that actually has file replacements + foreach (var item in previousData.FileReplacements) + { + previousData.FileReplacements[item.Key] = new HashSet(item.Value.Where(v => v.HasFileReplacement).OrderBy(v => v.ResolvedPath, StringComparer.Ordinal), FileReplacementComparer.Instance); + } + + st.Stop(); + Logger.Info("Building character data for " + objectKind + " took " + st.ElapsedMilliseconds + "ms"); + return previousData; + } + + private unsafe (HashSet forwardResolve, HashSet reverseResolve) BuildDataFromModel(ObjectKind objectKind, nint charaPointer, CancellationToken token) + { + HashSet forwardResolve = new(StringComparer.Ordinal); + HashSet reverseResolve = new(StringComparer.Ordinal); + 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; + } + + token.ThrowIfCancellationRequested(); + + AddReplacementsFromRenderModel(mdl, forwardResolve, reverseResolve); + } + + if (objectKind == ObjectKind.Player) + { + AddPlayerSpecificReplacements(objectKind, charaPointer, human, forwardResolve, reverseResolve); + } + + return (forwardResolve, reverseResolve); + } + + private unsafe void AddPlayerSpecificReplacements(ObjectKind objectKind, IntPtr charaPointer, Human* human, HashSet forwardResolve, HashSet reverseResolve) + { + var weaponObject = (Weapon*)((Object*)human)->ChildObject; + + if ((IntPtr)weaponObject != IntPtr.Zero) + { + var mainHandWeapon = weaponObject->WeaponRenderModel->RenderModel; + + AddReplacementsFromRenderModel(mainHandWeapon, forwardResolve, reverseResolve); + + foreach (var item in _transientResourceManager.GetTransientResources((IntPtr)weaponObject)) + { + Logger.Verbose("Found transient weapon resource: " + item); + forwardResolve.Add(item); + } + + if (weaponObject->NextSibling != (IntPtr)weaponObject) + { + var offHandWeapon = ((Weapon*)weaponObject->NextSibling)->WeaponRenderModel->RenderModel; + + AddReplacementsFromRenderModel(offHandWeapon, forwardResolve, reverseResolve); + + foreach (var item in _transientResourceManager.GetTransientResources((IntPtr)offHandWeapon)) + { + Logger.Verbose("Found transient offhand weapon resource: " + item); + forwardResolve.Add(item); + } + } + } + + AddReplacementSkeleton(((HumanExt*)human)->Human.RaceSexId, forwardResolve); + try + { + AddReplacementsFromTexture(new ByteString(((HumanExt*)human)->Decal->FileName()).ToString(), forwardResolve, reverseResolve, doNotReverseResolve: false); + } + catch + { + Logger.Warn("Could not get Decal data"); + } + try + { + AddReplacementsFromTexture(new ByteString(((HumanExt*)human)->LegacyBodyDecal->FileName()).ToString(), forwardResolve, reverseResolve, doNotReverseResolve: false); + } + catch + { + Logger.Warn("Could not get Legacy Body Decal Data"); + } + } + + + private unsafe void AddReplacementsFromRenderModel(RenderModel* mdl, HashSet forwardResolve, HashSet reverseResolve) { if (mdl == null || mdl->ResourceHandle == null || mdl->ResourceHandle->Category != ResourceCategory.Chara) { @@ -113,18 +274,18 @@ public class CharacterDataFactory } Logger.Verbose("Checking File Replacement for Model " + mdlPath); - AddResolvePath(mdlPath); + reverseResolve.Add(mdlPath); for (var mtrlIdx = 0; mtrlIdx < mdl->MaterialCount; mtrlIdx++) { var mtrl = (Material*)mdl->Materials[mtrlIdx]; if (mtrl == null) continue; - AddReplacementsFromMaterial(mtrl); + AddReplacementsFromMaterial(mtrl, forwardResolve, reverseResolve); } } - private unsafe void AddReplacementsFromMaterial(Material* mtrl) + private unsafe void AddReplacementsFromMaterial(Material* mtrl, HashSet forwardResolve, HashSet reverseResolve) { string fileName; try @@ -141,8 +302,7 @@ public class CharacterDataFactory Logger.Verbose("Checking File Replacement for Material " + fileName); var mtrlPath = fileName.Split("|")[2]; - - AddResolvePath(mtrlPath); + reverseResolve.Add(mtrlPath); var mtrlResourceHandle = (MtrlResource*)mtrl->ResourceHandle; for (var resIdx = 0; resIdx < mtrlResourceHandle->NumTex; resIdx++) @@ -161,14 +321,14 @@ public class CharacterDataFactory Logger.Verbose("Checking File Replacement for Texture " + texPath); - AddReplacementsFromTexture(texPath); + AddReplacementsFromTexture(texPath, forwardResolve, reverseResolve); } try { var shpkPath = "shader/sm5/shpk/" + new ByteString(mtrlResourceHandle->ShpkString).ToString(); Logger.Verbose("Checking File Replacement for Shader " + shpkPath); - AddReplacementsFromShader(shpkPath); + forwardResolve.Add(shpkPath); } catch { @@ -176,137 +336,51 @@ public class CharacterDataFactory } } - private void AddReplacement(string varPath, bool doNotReverseResolve = false) - { - if (varPath.IsNullOrEmpty()) return; - - AddResolvePath(varPath, doNotReverseResolve); - } - - private void AddReplacementsFromShader(string shpkPath) - { - if (string.IsNullOrEmpty(shpkPath)) return; - - AddResolvePath(shpkPath, doNotReverseResolve: true); - } - - private void AddReplacementsFromTexture(string texPath, bool doNotReverseResolve = true) + private void AddReplacementsFromTexture(string texPath, HashSet forwardResolve, HashSet reverseResolve, bool doNotReverseResolve = true) { if (string.IsNullOrEmpty(texPath)) return; - AddResolvePath(texPath, doNotReverseResolve); + if (doNotReverseResolve) + forwardResolve.Add(texPath); + else + reverseResolve.Add(texPath); if (texPath.Contains("/--", StringComparison.Ordinal)) return; - AddResolvePath(texPath.Insert(texPath.LastIndexOf('/') + 1, "--"), doNotReverseResolve); + var dx11Path = texPath.Insert(texPath.LastIndexOf('/') + 1, "--"); + if (doNotReverseResolve) + forwardResolve.Add(dx11Path); + else + reverseResolve.Add(dx11Path); } - private unsafe CharacterData CreateCharacterData(CharacterData previousData, GameObjectHandler playerRelatedObject, CancellationToken token) + private void AddReplacementSkeleton(ushort raceSexId, HashSet forwardResolve) { - var objectKind = playerRelatedObject.ObjectKind; - var charaPointer = playerRelatedObject.Address; + string raceSexIdString = raceSexId.ToString("0000"); - if (!previousData.FileReplacements.ContainsKey(objectKind)) - { - previousData.FileReplacements[objectKind] = new(FileReplacementComparer.Instance); - } - - _dalamudUtil.WaitWhileCharacterIsDrawing(playerRelatedObject.ObjectKind.ToString(), playerRelatedObject.Address, 30000, ct: token); - - Stopwatch st = Stopwatch.StartNew(); - - Logger.Debug("Handling unprocessed update for " + objectKind); - - if (previousData.FileReplacements.ContainsKey(objectKind)) - { - previousData.FileReplacements[objectKind].Clear(); - } - - 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.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; - } - - token.ThrowIfCancellationRequested(); - - AddReplacementsFromRenderModel(mdl); - } - - if (objectKind == ObjectKind.Player) - { - AddPlayerSpecificReplacements(objectKind, charaPointer, human); - } - - Dictionary> resolvedPaths = GetFileReplacementsFromPaths(); - previousData.FileReplacements[objectKind] = new HashSet(resolvedPaths.Select(c => new FileReplacement(c.Value, c.Key, _fileCacheManager)), FileReplacementComparer.Instance) - .Where(p => p.HasFileReplacement).ToHashSet(); - - previousData.ManipulationString = _ipcManager.PenumbraGetMetaManipulations(); - previousData.GlamourerString[objectKind] = _ipcManager.GlamourerGetCharacterCustomization(charaPointer); - previousData.HeelsOffset = _ipcManager.GetHeelsOffset(); - previousData.CustomizePlusScale = _ipcManager.GetCustomizePlusScale(); - previousData.PalettePlusPalette = _ipcManager.PalettePlusBuildPalette(); - - Logger.Debug("== Static Replacements =="); - foreach (var item in previousData.FileReplacements[objectKind].Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase)) - { - Logger.Debug(item.ToString()); - } - - if (objectKind == ObjectKind.Pet) - { - foreach (var item in previousData.FileReplacements[objectKind].Where(i => i.HasFileReplacement).SelectMany(p => p.GamePaths)) - { - _transientResourceManager.AddSemiTransientResource(objectKind, item); - } - } - - Logger.Debug("Handling transient update for " + objectKind); - _transientResourceManager.ClearTransientPaths(charaPointer, previousData.FileReplacements[objectKind].SelectMany(c => c.GamePaths).ToList()); - - _pathsToForwardResolve.Clear(); - _pathsToReverseResolve.Clear(); - - ManageSemiTransientData(objectKind, charaPointer); - - var resolvedTransientPaths = GetFileReplacementsFromPaths(); - Logger.Debug("== Transient Replacements =="); - foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement(c.Value, c.Key, _fileCacheManager)).OrderBy(f => f.ResolvedPath, StringComparer.Ordinal)) - { - Logger.Debug(replacement.ToString()); - previousData.FileReplacements[objectKind].Add(replacement); - } - - _transientResourceManager.CleanSemiTransientResources(objectKind, previousData.FileReplacements[objectKind].ToList()); - - foreach (var item in previousData.FileReplacements) - { - previousData.FileReplacements[item.Key] = new HashSet(item.Value.Where(v => v.HasFileReplacement).OrderBy(v => v.ResolvedPath, StringComparer.Ordinal), FileReplacementComparer.Instance); - } - - st.Stop(); - Logger.Info("Building character data for " + objectKind + " took " + st.ElapsedMilliseconds + "ms"); - return previousData; + string skeletonPath = $"chara/human/c{raceSexIdString}/skeleton/base/b0001/skl_c{raceSexIdString}b0001.sklb"; + forwardResolve.Add(skeletonPath); } - private Dictionary> GetFileReplacementsFromPaths() + private HashSet ManageSemiTransientData(ObjectKind objectKind, IntPtr charaPointer) { - var forwardPaths = _pathsToForwardResolve.ToArray(); - var reversePaths = _pathsToReverseResolve.ToArray(); + _transientResourceManager.PersistTransientResources(charaPointer, objectKind); + + HashSet pathsToResolve = new(StringComparer.Ordinal); + foreach (var path in _transientResourceManager.GetSemiTransientResources(objectKind).Where(path => !string.IsNullOrEmpty(path))) + { + pathsToResolve.Add(path); + } + + return pathsToResolve; + } + + private Dictionary> GetFileReplacementsFromPaths(HashSet forwardResolve, HashSet reverseResolve) + { + var forwardPaths = forwardResolve.ToArray(); + var reversePaths = reverseResolve.ToArray(); Dictionary> resolvedPaths = new(StringComparer.Ordinal); - var result = _ipcManager.PenumbraResolvePaths(_pathsToForwardResolve.ToArray(), _pathsToReverseResolve.ToArray()); + var result = _ipcManager.PenumbraResolvePaths(forwardPaths, reversePaths); for (int i = 0; i < forwardPaths.Length; i++) { var filePath = result.forward[i].ToLowerInvariant(); @@ -335,82 +409,4 @@ public class CharacterDataFactory return resolvedPaths; } - - private unsafe void ManageSemiTransientData(ObjectKind objectKind, IntPtr charaPointer) - { - _transientResourceManager.PersistTransientResources(charaPointer, objectKind); - - foreach (var item in _transientResourceManager.GetSemiTransientResources(objectKind)) - { - AddResolvePath(item, true); - } - } - - private unsafe void AddPlayerSpecificReplacements(ObjectKind objectKind, IntPtr charaPointer, Human* human) - { - var weaponObject = (Weapon*)((Object*)human)->ChildObject; - - if ((IntPtr)weaponObject != IntPtr.Zero) - { - var mainHandWeapon = weaponObject->WeaponRenderModel->RenderModel; - - AddReplacementsFromRenderModel(mainHandWeapon); - - foreach (var item in _transientResourceManager.GetTransientResources((IntPtr)weaponObject)) - { - Logger.Verbose("Found transient weapon resource: " + item); - AddReplacement(item, doNotReverseResolve: true); - } - - if (weaponObject->NextSibling != (IntPtr)weaponObject) - { - var offHandWeapon = ((Weapon*)weaponObject->NextSibling)->WeaponRenderModel->RenderModel; - - AddReplacementsFromRenderModel(offHandWeapon); - - foreach (var item in _transientResourceManager.GetTransientResources((IntPtr)offHandWeapon)) - { - Logger.Verbose("Found transient offhand weapon resource: " + item); - AddReplacement(item, doNotReverseResolve: true); - } - } - } - - AddReplacementSkeleton(((HumanExt*)human)->Human.RaceSexId); - try - { - AddReplacementsFromTexture(new ByteString(((HumanExt*)human)->Decal->FileName()).ToString(), doNotReverseResolve: false); - } - catch - { - Logger.Warn("Could not get Decal data"); - } - try - { - AddReplacementsFromTexture(new ByteString(((HumanExt*)human)->LegacyBodyDecal->FileName()).ToString(), doNotReverseResolve: false); - } - catch - { - Logger.Warn("Could not get Legacy Body Decal Data"); - } - } - - private void AddReplacementSkeleton(ushort raceSexId) - { - string raceSexIdString = raceSexId.ToString("0000"); - - string skeletonPath = $"chara/human/c{raceSexIdString}/skeleton/base/b0001/skl_c{raceSexIdString}b0001.sklb"; - - AddResolvePath(skeletonPath, doNotReverseResolve: true); - } - - private void AddResolvePath(string path, bool doNotReverseResolve = false) - { - if (string.IsNullOrEmpty(path)) return; - if (doNotReverseResolve) _pathsToForwardResolve.Add(path.ToLowerInvariant()); - else _pathsToReverseResolve.Add(path.ToLowerInvariant()); - } - - private readonly HashSet _pathsToForwardResolve = new(StringComparer.Ordinal); - private readonly HashSet _pathsToReverseResolve = new(StringComparer.Ordinal); } diff --git a/MareSynchronos/Managers/CacheCreationService.cs b/MareSynchronos/Managers/CacheCreationService.cs index aaeb422..e357690 100644 --- a/MareSynchronos/Managers/CacheCreationService.cs +++ b/MareSynchronos/Managers/CacheCreationService.cs @@ -97,13 +97,13 @@ public class CacheCreationService : MediatorSubscriberBase, IDisposable { var toCreate = _cachesToCreate.ToList(); _cachesToCreate.Clear(); - _cacheCreationTask = Task.Run(() => + _cacheCreationTask = Task.Run(async () => { try { foreach (var obj in toCreate) { - var data = _characterDataFactory.BuildCharacterData(_lastCreatedData, obj.Value, _cts.Token); + var data = await _characterDataFactory.BuildCharacterData(_lastCreatedData, obj.Value, _cts.Token).ConfigureAwait(false); } Mediator.Publish(new CharacterDataCreatedMessage(_lastCreatedData)); } diff --git a/MareSynchronos/Managers/TransientResourceManager.cs b/MareSynchronos/Managers/TransientResourceManager.cs index 55288b0..ff70ed6 100644 --- a/MareSynchronos/Managers/TransientResourceManager.cs +++ b/MareSynchronos/Managers/TransientResourceManager.cs @@ -86,7 +86,7 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable } } - public void CleanSemiTransientResources(ObjectKind objectKind, List? fileReplacement = null) + public void CleanUpSemiTransientResources(ObjectKind objectKind, List? fileReplacement = null) { if (SemiTransientResources.ContainsKey(objectKind)) { @@ -98,7 +98,6 @@ public class TransientResourceManager : MediatorSubscriberBase, IDisposable foreach (var replacement in fileReplacement.Where(p => !p.HasFileReplacement).SelectMany(p => p.GamePaths).ToList()) { - SemiTransientResources[objectKind].RemoveWhere(p => string.Equals(p, replacement, StringComparison.OrdinalIgnoreCase)); } }