using MareSynchronos.API.Data.Enum; using MareSynchronos.MareConfiguration; using MareSynchronos.MareConfiguration.Configurations; 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 = []; private ConcurrentDictionary>? _semiTransientResources = null; private uint _lastClassJobId = uint.MaxValue; 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 (!msg.OwnedObject) return; _playerRelatedPointers.Add(msg.GameObjectHandler); }); Mediator.Subscribe(this, (msg) => { if (!msg.OwnedObject) return; _playerRelatedPointers.Remove(msg.GameObjectHandler); }); } private TransientConfig.TransientPlayerConfig PlayerConfig { get { if (!_configurationService.Current.TransientConfigs.TryGetValue(PlayerPersistentDataKey, out var transientConfig)) { _configurationService.Current.TransientConfigs[PlayerPersistentDataKey] = transientConfig = new(); } return transientConfig; } } private string PlayerPersistentDataKey => _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult() + "_" + _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult(); private ConcurrentDictionary> SemiTransientResources { get { if (_semiTransientResources == null) { _semiTransientResources = new(); PlayerConfig.JobSpecificCache.TryGetValue(_dalamudUtil.ClassJobId, out var jobSpecificData); _semiTransientResources[ObjectKind.Player] = PlayerConfig.GlobalPersistentCache.Concat(jobSpecificData ?? []).ToHashSet(StringComparer.Ordinal); PlayerConfig.JobSpecificPetCache.TryGetValue(_dalamudUtil.ClassJobId, out var petSpecificData); _semiTransientResources[ObjectKind.Pet] = [.. petSpecificData ?? []]; } 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()) { PlayerConfig.RemovePath(replacement); } _configurationService.Save(); } } public HashSet GetSemiTransientResources(ObjectKind objectKind) { SemiTransientResources.TryGetValue(objectKind, out var result); return result ?? new HashSet(StringComparer.Ordinal); } public void PersistTransientResources(ObjectKind objectKind) { if (!SemiTransientResources.TryGetValue(objectKind, out HashSet? semiTransientResources)) { SemiTransientResources[objectKind] = semiTransientResources = new(StringComparer.Ordinal); } if (!TransientResources.TryGetValue(objectKind, out var resources)) { return; } var transientResources = resources.ToList(); Logger.LogDebug("Persisting {count} transient resources", transientResources.Count); List newlyAddedGamePaths = resources.Except(semiTransientResources, StringComparer.Ordinal).ToList(); foreach (var gamePath in transientResources) { semiTransientResources.Add(gamePath); } if (objectKind == ObjectKind.Player && newlyAddedGamePaths.Any()) { foreach (var item in newlyAddedGamePaths.Where(f => !string.IsNullOrEmpty(f))) { PlayerConfig.AddOrElevate(_dalamudUtil.ClassJobId, item); } _configurationService.Save(); } else if (objectKind == ObjectKind.Pet && newlyAddedGamePaths.Any()) { foreach (var item in newlyAddedGamePaths.Where(f => !string.IsNullOrEmpty(f))) { if (!PlayerConfig.JobSpecificPetCache.TryGetValue(_dalamudUtil.ClassJobId, out var petPerma)) { PlayerConfig.JobSpecificPetCache[_dalamudUtil.ClassJobId] = petPerma = []; } petPerma.Add(item); } _configurationService.Save(); } TransientResources[objectKind].Clear(); } public void RemoveTransientResource(ObjectKind objectKind, string path) { if (SemiTransientResources.TryGetValue(objectKind, out var resources)) { resources.RemoveWhere(f => string.Equals(path, f, StringComparison.Ordinal)); if (objectKind == ObjectKind.Player) { PlayerConfig.RemovePath(path); _configurationService.Save(); } } } internal bool AddTransientResource(ObjectKind objectKind, string item) { if (SemiTransientResources.TryGetValue(objectKind, out var semiTransient) && semiTransient != null && semiTransient.Contains(item)) return false; if (!TransientResources.TryGetValue(objectKind, out HashSet? value)) { value = new HashSet(StringComparer.Ordinal); TransientResources[objectKind] = value; } value.Add(item.ToLowerInvariant()); return true; } internal void ClearTransientPaths(ObjectKind objectKind, List list) { if (TransientResources.TryGetValue(objectKind, 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); } bool reloadSemiTransient = false; if (objectKind == ObjectKind.Player && SemiTransientResources.TryGetValue(objectKind, out var semiset)) { foreach (var file in semiset.Where(p => list.Contains(p, StringComparer.OrdinalIgnoreCase))) { Logger.LogTrace("Removing From Transient: {file}", file); PlayerConfig.RemovePath(file); } int removed = semiset.RemoveWhere(p => list.Contains(p, StringComparer.OrdinalIgnoreCase)); Logger.LogInformation("Removed {removed} previously existing semi transient paths", removed); if (removed > 0) { reloadSemiTransient = true; _configurationService.Save(); } } if (reloadSemiTransient) _semiTransientResources = null; } protected override void Dispose(bool disposing) { base.Dispose(disposing); TransientResources.Clear(); SemiTransientResources.Clear(); } private void DalamudUtil_FrameworkUpdate() { _cachedFrameAddresses = new(_playerRelatedPointers.Where(k => k.Address != nint.Zero).ToDictionary(c => c.CurrentAddress(), c => c.ObjectKind)); lock (_cacheAdditionLock) { _cachedHandledPaths.Clear(); } if (_lastClassJobId != _dalamudUtil.ClassJobId) { _lastClassJobId = _dalamudUtil.ClassJobId; if (SemiTransientResources.TryGetValue(ObjectKind.Pet, out HashSet? value)) { value?.Clear(); } // reload config for current new classjob PlayerConfig.JobSpecificCache.TryGetValue(_dalamudUtil.ClassJobId, out var jobSpecificData); SemiTransientResources[ObjectKind.Player] = PlayerConfig.GlobalPersistentCache.Concat(jobSpecificData ?? []).ToHashSet(StringComparer.OrdinalIgnoreCase); PlayerConfig.JobSpecificPetCache.TryGetValue(_dalamudUtil.ClassJobId, out var petSpecificData); SemiTransientResources[ObjectKind.Pet] = [.. petSpecificData ?? []]; } foreach (var kind in Enum.GetValues(typeof(ObjectKind))) { if (!_cachedFrameAddresses.Any(k => k.Value == (ObjectKind)kind) && TransientResources.Remove((ObjectKind)kind, out _)) { Logger.LogDebug("Object not present anymore: {kind}", kind.ToString()); } } } 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; } // ^ all of the code above is just to sanitize the data if (!TransientResources.TryGetValue(objectKind, out HashSet? value)) { value = new(StringComparer.OrdinalIgnoreCase); TransientResources[objectKind] = 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)); }); } } private CancellationTokenSource _sendTransientCts = new(); }