From c1940767bf071a14e5f95bbf7fac389a03af4066 Mon Sep 17 00:00:00 2001 From: Stanley Dimant Date: Mon, 10 Feb 2025 00:52:03 +0100 Subject: [PATCH] fix an issue where animations get removed/added through mod changes don't reload the config potentially fix mdl/mtrl/tex issues of recorded transients why did this even crash to begin with dunno what's wrong with pets (fuck pets, not literally) --- .../FileCache/TransientResourceManager.cs | 110 +++++++++++------- .../Interop/Ipc/IpcCallerHonorific.cs | 7 +- .../PlayerData/Factories/PlayerDataFactory.cs | 62 +++++----- 3 files changed, 104 insertions(+), 75 deletions(-) diff --git a/MareSynchronos/FileCache/TransientResourceManager.cs b/MareSynchronos/FileCache/TransientResourceManager.cs index 99f5581..3d1b433 100644 --- a/MareSynchronos/FileCache/TransientResourceManager.cs +++ b/MareSynchronos/FileCache/TransientResourceManager.cs @@ -17,8 +17,8 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase private readonly HashSet _cachedHandledPaths = new(StringComparer.Ordinal); private readonly TransientConfigService _configurationService; private readonly DalamudUtilService _dalamudUtil; - private readonly string[] _fileTypesToHandle = ["tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp", "shpk"]; - private readonly string[] _fileTypesToHandleRecording = ["tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp", "shpk", "tex", "mdl", "mtrl"]; + private readonly string[] _handledFileTypes = ["tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp", "shpk"]; + private readonly string[] _handledRecordingFileTypes = ["tex", "mdl", "mtrl"]; private readonly HashSet _playerRelatedPointers = []; private ConcurrentDictionary _cachedFrameAddresses = []; private ConcurrentDictionary>? _semiTransientResources = null; @@ -80,19 +80,27 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase public void CleanUpSemiTransientResources(ObjectKind objectKind, List? fileReplacement = null) { - if (SemiTransientResources.TryGetValue(objectKind, out HashSet? value)) + if (!SemiTransientResources.TryGetValue(objectKind, out HashSet? value)) + return; + + if (fileReplacement == null) { - if (fileReplacement == null) - { - value.Clear(); - return; - } + value.Clear(); + return; + } - foreach (var replacement in fileReplacement.Where(p => !p.HasFileReplacement).SelectMany(p => p.GamePaths).ToList()) - { - PlayerConfig.RemovePath(replacement); - } + int removedPaths = 0; + foreach (var replacement in fileReplacement.Where(p => !p.HasFileReplacement).SelectMany(p => p.GamePaths).ToList()) + { + removedPaths++; + PlayerConfig.RemovePath(replacement); + value.Remove(replacement); + } + if (removedPaths > 0) + { + Logger.LogTrace("Removed {amount} of SemiTransient paths during CleanUp, Saving from {name}", removedPaths, nameof(CleanUpSemiTransientResources)); + // force reload semi transient resources _configurationService.Save(); } } @@ -124,27 +132,33 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase semiTransientResources.Add(gamePath); } - if (objectKind == ObjectKind.Player && newlyAddedGamePaths.Any()) + bool saveConfig = false; + if (objectKind == ObjectKind.Player && newlyAddedGamePaths.Count != 0) { + saveConfig = true; foreach (var item in newlyAddedGamePaths.Where(f => !string.IsNullOrEmpty(f))) { PlayerConfig.AddOrElevate(_dalamudUtil.ClassJobId, item); } - - _configurationService.Save(); } - else if (objectKind == ObjectKind.Pet && newlyAddedGamePaths.Any()) + else if (objectKind == ObjectKind.Pet && newlyAddedGamePaths.Count != 0) { - foreach (var item in newlyAddedGamePaths.Where(f => !string.IsNullOrEmpty(f))) - { - if (!PlayerConfig.JobSpecificPetCache.TryGetValue(_dalamudUtil.ClassJobId, out var petPerma)) - { - PlayerConfig.JobSpecificPetCache[_dalamudUtil.ClassJobId] = petPerma = []; - } + saveConfig = true; - petPerma.Add(item); + if (!PlayerConfig.JobSpecificPetCache.TryGetValue(_dalamudUtil.ClassJobId, out var petPerma)) + { + PlayerConfig.JobSpecificPetCache[_dalamudUtil.ClassJobId] = petPerma = []; } + foreach (var item in newlyAddedGamePaths.Where(f => !string.IsNullOrEmpty(f))) + { + petPerma.Add(item); + } + } + + if (saveConfig) + { + Logger.LogTrace("Saving transient.json from {method}", nameof(PersistTransientResources)); _configurationService.Save(); } @@ -159,6 +173,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase if (objectKind == ObjectKind.Player) { PlayerConfig.RemovePath(path); + Logger.LogTrace("Saving transient.json from {method}", nameof(RemoveTransientResource)); _configurationService.Save(); } } @@ -169,18 +184,24 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase if (SemiTransientResources.TryGetValue(objectKind, out var semiTransient) && semiTransient != null && semiTransient.Contains(item)) return false; - if (!TransientResources.TryGetValue(objectKind, out HashSet? value)) + if (!TransientResources.TryGetValue(objectKind, out HashSet? transientResource)) { - value = new HashSet(StringComparer.Ordinal); - TransientResources[objectKind] = value; + transientResource = new HashSet(StringComparer.Ordinal); + TransientResources[objectKind] = transientResource; } - value.Add(item.ToLowerInvariant()); - return true; + return transientResource.Add(item.ToLowerInvariant()); } internal void ClearTransientPaths(ObjectKind objectKind, List list) { + // ignore all recording only datatypes + int recordingOnlyRemoved = list.RemoveAll(entry => _handledRecordingFileTypes.Any(ext => entry.EndsWith(ext, StringComparison.OrdinalIgnoreCase))); + if (recordingOnlyRemoved > 0) + { + Logger.LogTrace("Ignored {0} game paths when clearing transients", recordingOnlyRemoved); + } + if (TransientResources.TryGetValue(objectKind, out var set)) { foreach (var file in set.Where(p => list.Contains(p, StringComparer.OrdinalIgnoreCase))) @@ -197,7 +218,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase { foreach (var file in semiset.Where(p => list.Contains(p, StringComparer.OrdinalIgnoreCase))) { - Logger.LogTrace("Removing From Transient: {file}", file); + Logger.LogTrace("Removing From SemiTransient: {file}", file); PlayerConfig.RemovePath(file); } @@ -206,6 +227,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase if (removed > 0) { reloadSemiTransient = true; + Logger.LogTrace("Saving transient.json from {method}", nameof(ClearTransientPaths)); _configurationService.Save(); } } @@ -295,10 +317,13 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase // ignore files that are the same var replacedGamePath = gamePath.ToLowerInvariant().Replace("\\", "/", StringComparison.OrdinalIgnoreCase); - if (string.Equals(filePath, replacedGamePath, StringComparison.OrdinalIgnoreCase)) return; + if (string.Equals(filePath, replacedGamePath, StringComparison.OrdinalIgnoreCase)) + { + return; + } // ignore files to not handle - var handledTypes = IsTransientRecording ? _fileTypesToHandleRecording : _fileTypesToHandle; + var handledTypes = IsTransientRecording ? _handledRecordingFileTypes.Concat(_handledFileTypes) : _handledFileTypes; if (!handledTypes.Any(type => gamePath.EndsWith(type, StringComparison.OrdinalIgnoreCase))) { lock (_cacheAdditionLock) @@ -320,29 +345,34 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase // ^ all of the code above is just to sanitize the data - if (!TransientResources.TryGetValue(objectKind, out HashSet? value)) + if (!TransientResources.TryGetValue(objectKind, out HashSet? transientResources)) { - value = new(StringComparer.OrdinalIgnoreCase); - TransientResources[objectKind] = value; + transientResources = new(StringComparer.OrdinalIgnoreCase); + TransientResources[objectKind] = transientResources; } var owner = _playerRelatedPointers.FirstOrDefault(f => f.Address == gameObjectAddress); bool alreadyTransient = false; - if (value.Contains(replacedGamePath) - || SemiTransientResources.SelectMany(k => k.Value).Any(f => string.Equals(f, gamePath, StringComparison.OrdinalIgnoreCase))) + bool transientContains = transientResources.Contains(replacedGamePath); + bool semiTransientContains = SemiTransientResources.SelectMany(k => k.Value).Any(f => string.Equals(f, gamePath, StringComparison.OrdinalIgnoreCase)); + if (transientContains || semiTransientContains) { if (!IsTransientRecording) - Logger.LogTrace("Not adding {replacedPath} : {filePath}", replacedGamePath, filePath); + Logger.LogTrace("Not adding {replacedPath} => {filePath}, Reason: Transient: {contains}, SemiTransient: {contains2}", replacedGamePath, filePath, + transientContains, semiTransientContains); alreadyTransient = true; } else { if (!IsTransientRecording) { - value.Add(replacedGamePath); - Logger.LogDebug("Adding {replacedGamePath} for {gameObject} ({filePath})", replacedGamePath, owner?.ToString() ?? gameObjectAddress.ToString("X"), filePath); - SendTransients(gameObjectAddress); + bool isAdded = transientResources.Add(replacedGamePath); + if (isAdded) + { + Logger.LogDebug("Adding {replacedGamePath} for {gameObject} ({filePath})", replacedGamePath, owner?.ToString() ?? gameObjectAddress.ToString("X"), filePath); + SendTransients(gameObjectAddress); + } } } diff --git a/MareSynchronos/Interop/Ipc/IpcCallerHonorific.cs b/MareSynchronos/Interop/Ipc/IpcCallerHonorific.cs index 0edb543..a4b39e7 100644 --- a/MareSynchronos/Interop/Ipc/IpcCallerHonorific.cs +++ b/MareSynchronos/Interop/Ipc/IpcCallerHonorific.cs @@ -80,11 +80,8 @@ public sealed class IpcCallerHonorific : IIpcCaller public async Task GetTitle() { if (!APIAvailable) return string.Empty; - return await _dalamudUtil.RunOnFrameworkThread(() => - { - string title = _honorificGetLocalCharacterTitle.InvokeFunc(); - return string.IsNullOrEmpty(title) ? string.Empty : Convert.ToBase64String(Encoding.UTF8.GetBytes(title)); - }).ConfigureAwait(false); + string title = await _dalamudUtil.RunOnFrameworkThread(() => _honorificGetLocalCharacterTitle.InvokeFunc()).ConfigureAwait(false); + return string.IsNullOrEmpty(title) ? string.Empty : Convert.ToBase64String(Encoding.UTF8.GetBytes(title)); } public async Task SetTitleAsync(IntPtr character, string honorificDataB64) diff --git a/MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs b/MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs index 7efefb4..13a0680 100644 --- a/MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs +++ b/MareSynchronos/PlayerData/Factories/PlayerDataFactory.cs @@ -112,22 +112,22 @@ public class PlayerDataFactory return ((Character*)playerPointer)->GameObject.DrawObject == null; } - private async Task CreateCharacterData(CharacterData previousData, GameObjectHandler playerRelatedObject, CancellationToken token) + private async Task CreateCharacterData(CharacterData data, GameObjectHandler playerRelatedObject, CancellationToken token) { var objectKind = playerRelatedObject.ObjectKind; _logger.LogDebug("Building character data for {obj}", playerRelatedObject); - if (!previousData.FileReplacements.TryGetValue(objectKind, out HashSet? value)) + if (!data.FileReplacements.TryGetValue(objectKind, out HashSet? value)) { - previousData.FileReplacements[objectKind] = new(FileReplacementComparer.Instance); + data.FileReplacements[objectKind] = new(FileReplacementComparer.Instance); } else { value.Clear(); } - previousData.CustomizePlusScale.Remove(objectKind); + data.CustomizePlusScale.Remove(objectKind); // wait until chara is not drawing and present so nothing spontaneously explodes await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: token).ConfigureAwait(false); @@ -151,13 +151,13 @@ public class PlayerDataFactory resolvedPaths = await _ipcManager.Penumbra.GetCharacterData(_logger, playerRelatedObject).ConfigureAwait(false); if (resolvedPaths == null) throw new InvalidOperationException("Penumbra returned null data"); - previousData.FileReplacements[objectKind] = + data.FileReplacements[objectKind] = new HashSet(resolvedPaths.Select(c => new FileReplacement([.. c.Value], c.Key)), FileReplacementComparer.Instance) .Where(p => p.HasFileReplacement).ToHashSet(); - previousData.FileReplacements[objectKind].RemoveWhere(c => c.GamePaths.Any(g => !CacheMonitor.AllowedFileExtensions.Any(e => g.EndsWith(e, StringComparison.OrdinalIgnoreCase)))); + data.FileReplacements[objectKind].RemoveWhere(c => c.GamePaths.Any(g => !CacheMonitor.AllowedFileExtensions.Any(e => g.EndsWith(e, StringComparison.OrdinalIgnoreCase)))); _logger.LogDebug("== Static Replacements =="); - foreach (var replacement in previousData.FileReplacements[objectKind].Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase)) + foreach (var replacement in data.FileReplacements[objectKind].Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase)) { _logger.LogDebug("=> {repl}", replacement); } @@ -168,7 +168,7 @@ public class PlayerDataFactory // or we get into redraw city for every change and nothing works properly if (objectKind == ObjectKind.Pet) { - foreach (var item in previousData.FileReplacements[ObjectKind.Pet].Where(i => i.HasFileReplacement).SelectMany(p => p.GamePaths)) + foreach (var item in data.FileReplacements[ObjectKind.Pet].Where(i => i.HasFileReplacement).SelectMany(p => p.GamePaths)) { if (_transientResourceManager.AddTransientResource(objectKind, item)) { @@ -176,14 +176,14 @@ public class PlayerDataFactory } } - _logger.LogTrace("Clearing {count} Static Replacements for Pet", previousData.FileReplacements[ObjectKind.Pet].Count); - previousData.FileReplacements[ObjectKind.Pet].Clear(); + _logger.LogTrace("Clearing {count} Static Replacements for Pet", data.FileReplacements[ObjectKind.Pet].Count); + data.FileReplacements[ObjectKind.Pet].Clear(); } _logger.LogDebug("Handling transient update for {obj}", playerRelatedObject); // remove all potentially gathered paths from the transient resource manager that are resolved through static resolving - _transientResourceManager.ClearTransientPaths(objectKind, previousData.FileReplacements[objectKind].SelectMany(c => c.GamePaths).ToList()); + _transientResourceManager.ClearTransientPaths(objectKind, data.FileReplacements[objectKind].SelectMany(c => c.GamePaths).ToList()); // get all remaining paths and resolve them var transientPaths = ManageSemiTransientData(objectKind); @@ -193,41 +193,43 @@ public class PlayerDataFactory foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)).OrderBy(f => f.ResolvedPath, StringComparer.Ordinal)) { _logger.LogDebug("=> {repl}", replacement); - previousData.FileReplacements[objectKind].Add(replacement); + data.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]]); + _transientResourceManager.CleanUpSemiTransientResources(objectKind, [.. data.FileReplacements[objectKind]]); // make sure we only return data that actually has file replacements - foreach (var item in previousData.FileReplacements) + foreach (var item in data.FileReplacements) { - previousData.FileReplacements[item.Key] = new HashSet(item.Value.Where(v => v.HasFileReplacement).OrderBy(v => v.ResolvedPath, StringComparer.Ordinal), FileReplacementComparer.Instance); + data.FileReplacements[item.Key] = new HashSet(item.Value.Where(v => v.HasFileReplacement).OrderBy(v => v.ResolvedPath, StringComparer.Ordinal), FileReplacementComparer.Instance); } // gather up data from ipc - previousData.ManipulationString = _ipcManager.Penumbra.GetMetaManipulations(); + data.ManipulationString = _ipcManager.Penumbra.GetMetaManipulations(); Task getHeelsOffset = _ipcManager.Heels.GetOffsetAsync(); Task getGlamourerData = _ipcManager.Glamourer.GetCharacterCustomizationAsync(playerRelatedObject.Address); Task getCustomizeData = _ipcManager.CustomizePlus.GetScaleAsync(playerRelatedObject.Address); Task getHonorificTitle = _ipcManager.Honorific.GetTitle(); - previousData.GlamourerString[playerRelatedObject.ObjectKind] = await getGlamourerData.ConfigureAwait(false); - _logger.LogDebug("Glamourer is now: {data}", previousData.GlamourerString[playerRelatedObject.ObjectKind]); + data.GlamourerString[playerRelatedObject.ObjectKind] = await getGlamourerData.ConfigureAwait(false); + _logger.LogDebug("Glamourer is now: {data}", data.GlamourerString[playerRelatedObject.ObjectKind]); var customizeScale = await getCustomizeData.ConfigureAwait(false); - previousData.CustomizePlusScale[playerRelatedObject.ObjectKind] = customizeScale ?? string.Empty; - _logger.LogDebug("Customize is now: {data}", previousData.CustomizePlusScale[playerRelatedObject.ObjectKind]); - previousData.HonorificData = await getHonorificTitle.ConfigureAwait(false); - _logger.LogDebug("Honorific is now: {data}", previousData.HonorificData); - previousData.HeelsData = await getHeelsOffset.ConfigureAwait(false); - _logger.LogDebug("Heels is now: {heels}", previousData.HeelsData); + data.CustomizePlusScale[playerRelatedObject.ObjectKind] = customizeScale ?? string.Empty; + _logger.LogDebug("Customize is now: {data}", data.CustomizePlusScale[playerRelatedObject.ObjectKind]); + data.HonorificData = await getHonorificTitle.ConfigureAwait(false); + _logger.LogDebug("Honorific is now: {data}", data.HonorificData); + data.HeelsData = await getHeelsOffset.ConfigureAwait(false); + _logger.LogDebug("Heels is now: {heels}", data.HeelsData); if (objectKind == ObjectKind.Player) { - previousData.PetNamesData = _ipcManager.PetNames.GetLocalNames(); - _logger.LogDebug("Pet Nicknames is now: {petnames}", previousData.PetNamesData); - previousData.MoodlesData = await _ipcManager.Moodles.GetStatusAsync(playerRelatedObject.Address).ConfigureAwait(false) ?? string.Empty; + data.MoodlesData = await _ipcManager.Moodles.GetStatusAsync(playerRelatedObject.Address).ConfigureAwait(false) ?? string.Empty; + _logger.LogDebug("Moodles is now: {moodles}", data.MoodlesData); + + data.PetNamesData = _ipcManager.PetNames.GetLocalNames(); + _logger.LogDebug("Pet Nicknames is now: {petnames}", data.PetNamesData); } - if (previousData.FileReplacements.TryGetValue(objectKind, out HashSet? fileReplacements)) + if (data.FileReplacements.TryGetValue(objectKind, out HashSet? fileReplacements)) { var toCompute = fileReplacements.Where(f => !f.IsFileSwap).ToArray(); _logger.LogDebug("Getting Hashes for {amount} Files", toCompute.Length); @@ -247,7 +249,7 @@ public class PlayerDataFactory { try { - await VerifyPlayerAnimationBones(boneIndices, previousData, objectKind).ConfigureAwait(false); + await VerifyPlayerAnimationBones(boneIndices, data, objectKind).ConfigureAwait(false); } catch (Exception e) { @@ -257,7 +259,7 @@ public class PlayerDataFactory _logger.LogInformation("Building character data for {obj} took {time}ms", objectKind, TimeSpan.FromTicks(DateTime.UtcNow.Ticks - start.Ticks).TotalMilliseconds); - return previousData; + return data; } private async Task VerifyPlayerAnimationBones(Dictionary>? boneIndices, CharacterData previousData, ObjectKind objectKind)