using MareSynchronos.API.Data.Enum; using MareSynchronos.MareConfiguration; using MareSynchronos.PlayerData.Data; using MareSynchronos.PlayerData.Handlers; using MareSynchronos.Services; using MareSynchronos.Services.Mediator; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; namespace MareSynchronos.FileCache; public sealed class TransientResourceManager : DisposableMediatorSubscriberBase { private readonly Lock _cacheAdditionLock = new(); 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 HashSet _playerRelatedPointers = []; private ConcurrentDictionary _cachedFrameAddresses = []; public TransientResourceManager(ILogger logger, TransientConfigService configurationService, DalamudUtilService dalamudUtil, MareMediator mediator) : base(logger, mediator) { _configurationService = configurationService; _dalamudUtil = dalamudUtil; Mediator.Subscribe(this, Manager_PenumbraResourceLoadEvent); Mediator.Subscribe(this, (_) => Manager_PenumbraModSettingChanged()); Mediator.Subscribe(this, (_) => DalamudUtil_FrameworkUpdate()); Mediator.Subscribe(this, (msg) => { if (_playerRelatedPointers.Contains(msg.GameObjectHandler)) { DalamudUtil_ClassJobChanged(); } }); Mediator.Subscribe(this, (msg) => { if (!msg.OwnedObject) return; _playerRelatedPointers.Add(msg.GameObjectHandler); }); Mediator.Subscribe(this, (msg) => { if (!msg.OwnedObject) return; _playerRelatedPointers.Remove(msg.GameObjectHandler); }); } private string PlayerPersistentDataKey => _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult() + "_" + _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult(); private ConcurrentDictionary>? _semiTransientResources = null; private ConcurrentDictionary> SemiTransientResources { get { if (_semiTransientResources == null) { _semiTransientResources = new(); _semiTransientResources.TryAdd(ObjectKind.Player, new HashSet(StringComparer.Ordinal)); if (_configurationService.Current.PlayerPersistentTransientCache.TryGetValue(PlayerPersistentDataKey, out var gamePaths)) { int restored = 0; foreach (var gamePath in gamePaths) { if (string.IsNullOrEmpty(gamePath)) continue; try { Logger.LogDebug("Loaded persistent transient resource {path}", gamePath); SemiTransientResources[ObjectKind.Player].Add(gamePath); restored++; } catch (Exception ex) { Logger.LogWarning(ex, "Error during loading persistent transient resource {path}", gamePath); } } Logger.LogDebug("Restored {restored}/{total} semi persistent resources", restored, gamePaths.Count); } } return _semiTransientResources; } } private ConcurrentDictionary> TransientResources { get; } = new(); public void CleanUpSemiTransientResources(ObjectKind objectKind, List? fileReplacement = null) { if (SemiTransientResources.TryGetValue(objectKind, out HashSet? value)) { if (fileReplacement == null) { value.Clear(); return; } foreach (var replacement in fileReplacement.Where(p => !p.HasFileReplacement).SelectMany(p => p.GamePaths).ToList()) { value.RemoveWhere(p => string.Equals(p, replacement, StringComparison.OrdinalIgnoreCase)); } } } public HashSet GetSemiTransientResources(ObjectKind objectKind) { if (SemiTransientResources.TryGetValue(objectKind, out var result)) { return result ?? new HashSet(StringComparer.Ordinal); } return new HashSet(StringComparer.Ordinal); } public List GetTransientResources(IntPtr gameObject) { if (TransientResources.TryGetValue(gameObject, out var result)) { return [.. result]; } return []; } public void PersistTransientResources(IntPtr gameObject, ObjectKind objectKind) { if (!SemiTransientResources.TryGetValue(objectKind, out HashSet? value)) { value = new HashSet(StringComparer.Ordinal); SemiTransientResources[objectKind] = value; } if (!TransientResources.TryGetValue(gameObject, out var resources)) { return; } var transientResources = resources.ToList(); Logger.LogDebug("Persisting {count} transient resources", transientResources.Count); foreach (var gamePath in transientResources) { value.Add(gamePath); } if (objectKind == ObjectKind.Player && SemiTransientResources.TryGetValue(ObjectKind.Player, out var fileReplacements)) { _configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey] = fileReplacements.Where(f => !string.IsNullOrEmpty(f)).ToHashSet(StringComparer.Ordinal); _configurationService.Save(); } TransientResources[gameObject].Clear(); } internal void AddSemiTransientResource(ObjectKind objectKind, string item) { if (!SemiTransientResources.TryGetValue(objectKind, out HashSet? value)) { value = new HashSet(StringComparer.Ordinal); SemiTransientResources[objectKind] = value; } value.Add(item.ToLowerInvariant()); } internal void ClearTransientPaths(IntPtr ptr, List list) { if (TransientResources.TryGetValue(ptr, out var set)) { foreach (var file in set.Where(p => list.Contains(p, StringComparer.OrdinalIgnoreCase))) { Logger.LogTrace("Removing From Transient: {file}", file); } int removed = set.RemoveWhere(p => list.Contains(p, StringComparer.OrdinalIgnoreCase)); Logger.LogInformation("Removed {removed} previously existing transient paths", removed); } } protected override void Dispose(bool disposing) { base.Dispose(disposing); try { TransientResources.Clear(); SemiTransientResources.Clear(); if (SemiTransientResources.TryGetValue(ObjectKind.Player, out HashSet? value)) { _configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey] = value; _configurationService.Save(); } } catch { } } private void DalamudUtil_ClassJobChanged() { if (SemiTransientResources.TryGetValue(ObjectKind.Pet, out HashSet? value)) { value?.Clear(); } } private void DalamudUtil_FrameworkUpdate() { _cachedFrameAddresses = _cachedFrameAddresses = new ConcurrentDictionary(_playerRelatedPointers.Where(k => k.Address != nint.Zero).ToDictionary(c => c.CurrentAddress(), c => c.ObjectKind)); lock (_cacheAdditionLock) { _cachedHandledPaths.Clear(); } foreach (var item in TransientResources.Where(item => !_dalamudUtil.IsGameObjectPresent(item.Key)).Select(i => i.Key).ToList()) { Logger.LogDebug("Object not present anymore: {addr}", item.ToString("X")); TransientResources.TryRemove(item, out _); } } private void Manager_PenumbraModSettingChanged() { _ = Task.Run(() => { Logger.LogDebug("Penumbra Mod Settings changed, verifying SemiTransientResources"); foreach (var item in _playerRelatedPointers) { Mediator.Publish(new TransientResourceChangedMessage(item.Address)); } }); } private void Manager_PenumbraResourceLoadEvent(PenumbraResourceLoadMessage msg) { var gamePath = msg.GamePath.ToLowerInvariant(); var gameObject = msg.GameObject; var filePath = msg.FilePath; // ignore files already processed this frame if (_cachedHandledPaths.Contains(gamePath)) return; lock (_cacheAdditionLock) { _cachedHandledPaths.Add(gamePath); } // replace individual mtrl stuff if (filePath.StartsWith("|", StringComparison.OrdinalIgnoreCase)) { filePath = filePath.Split("|")[2]; } // replace filepath filePath = filePath.ToLowerInvariant().Replace("\\", "/", StringComparison.OrdinalIgnoreCase); // ignore files that are the same var replacedGamePath = gamePath.ToLowerInvariant().Replace("\\", "/", StringComparison.OrdinalIgnoreCase); if (string.Equals(filePath, replacedGamePath, StringComparison.OrdinalIgnoreCase)) return; // ignore files to not handle if (!_fileTypesToHandle.Any(type => gamePath.EndsWith(type, StringComparison.OrdinalIgnoreCase))) { lock (_cacheAdditionLock) { _cachedHandledPaths.Add(gamePath); } return; } // ignore files not belonging to anything player related if (!_cachedFrameAddresses.TryGetValue(gameObject, out var objectKind)) { lock (_cacheAdditionLock) { _cachedHandledPaths.Add(gamePath); } return; } if (!TransientResources.TryGetValue(gameObject, out HashSet? value)) { value = new(StringComparer.OrdinalIgnoreCase); TransientResources[gameObject] = value; } if (value.Contains(replacedGamePath) || SemiTransientResources.SelectMany(k => k.Value).Any(f => string.Equals(f, gamePath, StringComparison.OrdinalIgnoreCase))) { Logger.LogTrace("Not adding {replacedPath} : {filePath}", replacedGamePath, filePath); } else { var thing = _playerRelatedPointers.FirstOrDefault(f => f.Address == gameObject); value.Add(replacedGamePath); Logger.LogDebug("Adding {replacedGamePath} for {gameObject} ({filePath})", replacedGamePath, thing?.ToString() ?? gameObject.ToString("X"), filePath); _ = Task.Run(async () => { _sendTransientCts?.Cancel(); _sendTransientCts?.Dispose(); _sendTransientCts = new(); var token = _sendTransientCts.Token; await Task.Delay(TimeSpan.FromSeconds(2), token).ConfigureAwait(false); Mediator.Publish(new TransientResourceChangedMessage(gameObject)); }); } } internal void RemoveTransientResource(ObjectKind objectKind, string path) { if (SemiTransientResources.TryGetValue(objectKind, out var resources)) { resources.RemoveWhere(f => string.Equals(path, f, StringComparison.OrdinalIgnoreCase)); _configurationService.Current.PlayerPersistentTransientCache[PlayerPersistentDataKey] = resources; _configurationService.Save(); } } private CancellationTokenSource _sendTransientCts = new(); }