using System.Collections.Concurrent; using Dalamud.Plugin; using LightlessSync.Interop.Ipc.Framework; using LightlessSync.Services; using LightlessSync.Services.Mediator; using Microsoft.Extensions.Logging; using Penumbra.Api.Enums; using Penumbra.Api.IpcSubscribers; namespace LightlessSync.Interop.Ipc.Penumbra; public sealed class PenumbraCollections : PenumbraBase { private readonly CreateTemporaryCollection _createNamedTemporaryCollection; private readonly AssignTemporaryCollection _assignTemporaryCollection; private readonly DeleteTemporaryCollection _removeTemporaryCollection; private readonly AddTemporaryMod _addTemporaryMod; private readonly RemoveTemporaryMod _removeTemporaryMod; private readonly GetCollections _getCollections; private readonly ConcurrentDictionary _activeTemporaryCollections = new(); private int _cleanupScheduled; public PenumbraCollections( ILogger logger, IDalamudPluginInterface pluginInterface, DalamudUtilService dalamudUtil, LightlessMediator mediator) : base(logger, pluginInterface, dalamudUtil, mediator) { _createNamedTemporaryCollection = new CreateTemporaryCollection(pluginInterface); _assignTemporaryCollection = new AssignTemporaryCollection(pluginInterface); _removeTemporaryCollection = new DeleteTemporaryCollection(pluginInterface); _addTemporaryMod = new AddTemporaryMod(pluginInterface); _removeTemporaryMod = new RemoveTemporaryMod(pluginInterface); _getCollections = new GetCollections(pluginInterface); } public override string Name => "Penumbra.Collections"; public async Task AssignTemporaryCollectionAsync(ILogger logger, Guid collectionId, int objectIndex) { if (!IsAvailable || collectionId == Guid.Empty) { return; } await DalamudUtil.RunOnFrameworkThread(() => { var result = _assignTemporaryCollection.Invoke(collectionId, objectIndex, forceAssignment: true); logger.LogTrace("Assigning Temp Collection {CollectionId} to index {ObjectIndex}, Success: {Result}", collectionId, objectIndex, result); return result; }).ConfigureAwait(false); } public async Task CreateTemporaryCollectionAsync(ILogger logger, string uid) { if (!IsAvailable) { return Guid.Empty; } var (collectionId, collectionName) = await DalamudUtil.RunOnFrameworkThread(() => { var name = $"Lightless_{uid}"; _createNamedTemporaryCollection.Invoke(name, name, out var tempCollectionId); logger.LogTrace("Creating Temp Collection {CollectionName}, GUID: {CollectionId}", name, tempCollectionId); return (tempCollectionId, name); }).ConfigureAwait(false); if (collectionId != Guid.Empty) { _activeTemporaryCollections[collectionId] = collectionName; } return collectionId; } public async Task RemoveTemporaryCollectionAsync(ILogger logger, Guid applicationId, Guid collectionId) { if (!IsAvailable || collectionId == Guid.Empty) { return; } await DalamudUtil.RunOnFrameworkThread(() => { logger.LogTrace("[{ApplicationId}] Removing temp collection for {CollectionId}", applicationId, collectionId); var result = _removeTemporaryCollection.Invoke(collectionId); logger.LogTrace("[{ApplicationId}] RemoveTemporaryCollection: {Result}", applicationId, result); }).ConfigureAwait(false); _activeTemporaryCollections.TryRemove(collectionId, out _); } public async Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collectionId, IReadOnlyDictionary modPaths) { if (!IsAvailable || collectionId == Guid.Empty) { return; } await DalamudUtil.RunOnFrameworkThread(() => { foreach (var mod in modPaths) { logger.LogTrace("[{ApplicationId}] Change: {From} => {To}", applicationId, mod.Key, mod.Value); } var removeResult = _removeTemporaryMod.Invoke("LightlessChara_Files", collectionId, 0); logger.LogTrace("[{ApplicationId}] Removing temp files mod for {CollectionId}, Success: {Result}", applicationId, collectionId, removeResult); var addResult = _addTemporaryMod.Invoke("LightlessChara_Files", collectionId, new Dictionary(modPaths), string.Empty, 0); logger.LogTrace("[{ApplicationId}] Setting temp files mod for {CollectionId}, Success: {Result}", applicationId, collectionId, addResult); }).ConfigureAwait(false); } public async Task SetManipulationDataAsync(ILogger logger, Guid applicationId, Guid collectionId, string manipulationData) { if (!IsAvailable || collectionId == Guid.Empty) { return; } await DalamudUtil.RunOnFrameworkThread(() => { logger.LogTrace("[{ApplicationId}] Manip: {Data}", applicationId, manipulationData); var result = _addTemporaryMod.Invoke("LightlessChara_Meta", collectionId, [], manipulationData, 0); logger.LogTrace("[{ApplicationId}] Setting temp meta mod for {CollectionId}, Success: {Result}", applicationId, collectionId, result); }).ConfigureAwait(false); } protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current) { if (current == IpcConnectionState.Available) { ScheduleCleanup(); } else if (previous == IpcConnectionState.Available && current != IpcConnectionState.Available) { Interlocked.Exchange(ref _cleanupScheduled, 0); } } private void ScheduleCleanup() { if (Interlocked.Exchange(ref _cleanupScheduled, 1) != 0) { return; } _ = Task.Run(CleanupTemporaryCollectionsAsync); } private async Task CleanupTemporaryCollectionsAsync() { if (!IsAvailable) { return; } try { var collections = await DalamudUtil.RunOnFrameworkThread(() => _getCollections.Invoke()).ConfigureAwait(false); foreach (var (collectionId, name) in collections) { if (!IsLightlessCollectionName(name) || _activeTemporaryCollections.ContainsKey(collectionId)) { continue; } Logger.LogDebug("Cleaning up stale temporary collection {CollectionName} ({CollectionId})", name, collectionId); var deleteResult = await DalamudUtil.RunOnFrameworkThread(() => { var result = (PenumbraApiEc)_removeTemporaryCollection.Invoke(collectionId); Logger.LogTrace("Cleanup RemoveTemporaryCollection result for {CollectionName} ({CollectionId}): {Result}", name, collectionId, result); return result; }).ConfigureAwait(false); if (deleteResult == PenumbraApiEc.Success) { _activeTemporaryCollections.TryRemove(collectionId, out _); } else { Logger.LogDebug("Skipped removing temporary collection {CollectionName} ({CollectionId}). Result: {Result}", name, collectionId, deleteResult); } } } catch (Exception ex) { Logger.LogWarning(ex, "Failed to clean up Penumbra temporary collections"); } } private static bool IsLightlessCollectionName(string? name) => !string.IsNullOrEmpty(name) && name.StartsWith("Lightless_", StringComparison.Ordinal); }