diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index 531b6333..64341157 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -31,69 +31,102 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer public ModCollection Create(string name, int index, ModCollection? duplicate) { - var newCollection = duplicate?.Duplicate(name, CurrentCollectionId, index) - ?? ModCollection.CreateEmpty(name, CurrentCollectionId, index, _modStorage.Count); - _collectionsByLocal[CurrentCollectionId] = newCollection; - CurrentCollectionId += 1; + var localId = AllocateNextId(); + var newCollection = duplicate?.Duplicate(name, localId, index) + ?? ModCollection.CreateEmpty(name, localId, index, _modStorage.Count); + AddAtLocalId(newCollection, localId); return newCollection; } public ModCollection CreateFromData(Guid id, string name, int version, Dictionary allSettings, IReadOnlyList inheritances) { + var localId = AllocateNextId(); var newCollection = ModCollection.CreateFromData(_saveService, _modStorage, - new ModCollectionIdentity(id, CurrentCollectionId, name, Count), version, allSettings, inheritances); - _collectionsByLocal[CurrentCollectionId] = newCollection; - CurrentCollectionId += 1; + new ModCollectionIdentity(id, localId, name, Count), version, allSettings, inheritances); + AddAtLocalId(newCollection, localId); return newCollection; } public ModCollection CreateTemporary(string name, int index, int globalChangeCounter) { - var newCollection = ModCollection.CreateTemporary(name, CurrentCollectionId, index, globalChangeCounter); - _collectionsByLocal[CurrentCollectionId] = newCollection; - CurrentCollectionId += 1; + var localId = AllocateNextId(); + var newCollection = ModCollection.CreateTemporary(name, localId, index, globalChangeCounter); + AddAtLocalId(newCollection, localId); return newCollection; } + /// Atomically add to _collectionLocal at the id given. + private void AddAtLocalId(ModCollection newCollection, LocalCollectionId id) + { + _collectionsByLocal.AddOrUpdate(id, + static (_, newColl) => newColl, + static (_, _, newColl) => newColl, + newCollection); + } + public void Delete(ModCollection collection) - => _collectionsByLocal.Remove(collection.Identity.LocalId); + => _collectionsByLocal.TryRemove(collection.Identity.LocalId, out _); /// The empty collection is always available at Index 0. private readonly List _collections = [ ModCollection.Empty, ]; + + private readonly Lock _collectionsLock = new(); /// A list of all collections ever created still existing by their local id. - private readonly Dictionary + private readonly ConcurrentDictionary _collectionsByLocal = new() { [LocalCollectionId.Zero] = ModCollection.Empty }; public readonly ModCollection DefaultNamed; - /// Incremented by 1 because the empty collection gets Zero. - public LocalCollectionId CurrentCollectionId { get; private set; } = LocalCollectionId.Zero + 1; + /// Starts at 1 because the empty collection gets Zero. + private int _currentCollectionIdValue = 1; + + /// Starts at 1 because the empty collection gets Zero. + public LocalCollectionId CurrentCollectionId => new(_currentCollectionIdValue); + + private LocalCollectionId AllocateNextId() + { + var newLocalId = new LocalCollectionId(_currentCollectionIdValue); + Interlocked.Increment(ref _currentCollectionIdValue); + return newLocalId; + } /// Default enumeration skips the empty collection. public IEnumerator GetEnumerator() - => _collections.Skip(1).GetEnumerator(); + => GetModSnapShot().ToList().GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); public int Count - => _collections.Count; + { + get + { + lock(_collectionsLock) + return _collections.Count; + } + } public ModCollection this[int index] - => _collections[index]; + { + get + { + lock(_collectionsLock) + return _collections[index]; + } + } /// Find a collection by its name. If the name is empty or None, the empty collection is returned. public bool ByName(string name, [NotNullWhen(true)] out ModCollection? collection) { if (name.Length != 0) - return _collections.FindFirst(c => string.Equals(c.Identity.Name, name, StringComparison.OrdinalIgnoreCase), out collection); - + lock(_collectionsLock) + return _collections.FindFirst(c => string.Equals(c.Identity.Name, name, StringComparison.OrdinalIgnoreCase), out collection); collection = ModCollection.Empty; return true; } @@ -102,8 +135,8 @@ public bool ByName(string name, [NotNullWhen(true)] out ModCollection? collectio public bool ById(Guid id, [NotNullWhen(true)] out ModCollection? collection) { if (id != Guid.Empty) - return _collections.FindFirst(c => c.Identity.Id == id, out collection); - + lock(_collectionsLock) + return _collections.FindFirst(c => c.Identity.Id == id, out collection); collection = ModCollection.Empty; return true; } @@ -155,8 +188,12 @@ public bool AddCollection(string name, ModCollection? duplicate) if (name.Length == 0) return false; - var newCollection = Create(name, _collections.Count, duplicate); - _collections.Add(newCollection); + ModCollection newCollection; + lock (_collectionsLock) + { + newCollection = Create(name, _collections.Count, duplicate); + _collections.Add(newCollection); + } _saveService.ImmediateSave(new ModCollectionSave(_modStorage, newCollection)); Penumbra.Messager.NotificationMessage($"Created new collection {newCollection.Identity.AnonymizedName}.", NotificationType.Success, false); _communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty); @@ -168,25 +205,28 @@ public bool AddCollection(string name, ModCollection? duplicate) /// public bool RemoveCollection(ModCollection collection) { - if (collection.Identity.Index <= ModCollection.Empty.Identity.Index || collection.Identity.Index >= _collections.Count) + lock (_collectionsLock) { - Penumbra.Messager.NotificationMessage("Can not remove the empty collection.", NotificationType.Error, false); - return false; - } + if (collection.Identity.Index <= ModCollection.Empty.Identity.Index || collection.Identity.Index >= _collections.Count) + { + Penumbra.Messager.NotificationMessage("Can not remove the empty collection.", NotificationType.Error, false); + return false; + } - if (collection.Identity.Index == DefaultNamed.Identity.Index) - { - Penumbra.Messager.NotificationMessage("Can not remove the default collection.", NotificationType.Error, false); - return false; - } + if (collection.Identity.Index == DefaultNamed.Identity.Index) + { + Penumbra.Messager.NotificationMessage("Can not remove the default collection.", NotificationType.Error, false); + return false; + } + + Delete(collection); + _saveService.ImmediateDelete(new ModCollectionSave(_modStorage, collection)); - Delete(collection); - _saveService.ImmediateDelete(new ModCollectionSave(_modStorage, collection)); - _collections.RemoveAt(collection.Identity.Index); - // Update indices. - for (var i = collection.Identity.Index; i < Count; ++i) - _collections[i].Identity.Index = i; - _collectionsByLocal.Remove(collection.Identity.LocalId); + _collections.RemoveAt(collection.Identity.Index); + // Update indices. + for (var i = collection.Identity.Index; i < _collections.Count; ++i) + _collections[i].Identity.Index = i; + } Penumbra.Messager.NotificationMessage($"Deleted collection {collection.Identity.AnonymizedName}.", NotificationType.Success, false); _communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty); @@ -299,26 +339,28 @@ private ModCollection SetDefaultNamedCollection() return collection; if (AddCollection(ModCollectionIdentity.DefaultCollectionName, null)) - return _collections[^1]; + return this[^1]; Penumbra.Messager.NotificationMessage( $"Unknown problem creating a collection with the name {ModCollectionIdentity.DefaultCollectionName}, which is required to exist.", NotificationType.Error); - return Count > 1 ? _collections[1] : _collections[0]; + return Count > 1 ? this[1] : this[0]; } /// Move all settings in all collections to unused settings. private void OnModDiscoveryStarted() { - foreach (var collection in this) + var snapshot = GetModSnapShot(); + foreach (var collection in snapshot) collection.Settings.PrepareModDiscovery(_modStorage); } /// Restore all settings in all collections to mods. private void OnModDiscoveryFinished() { + var snapshot = GetModSnapShot(); // Re-apply all mod settings. - foreach (var collection in this) + foreach (var collection in snapshot) collection.Settings.ApplyModSettings(collection, _saveService, _modStorage); } @@ -326,22 +368,23 @@ private void OnModDiscoveryFinished() private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, DirectoryInfo? newDirectory) { + var snapshot = GetModSnapShot(); switch (type) { case ModPathChangeType.Added: - foreach (var collection in this) + foreach (var collection in snapshot) collection.Settings.AddMod(mod); break; case ModPathChangeType.Deleted: - foreach (var collection in this) + foreach (var collection in snapshot) collection.Settings.RemoveMod(mod); break; case ModPathChangeType.Moved: - foreach (var collection in this.Where(collection => collection.GetOwnSettings(mod.Index) != null)) + foreach (var collection in snapshot.Where(collection => collection.GetOwnSettings(mod.Index) != null)) _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); break; case ModPathChangeType.Reloaded: - foreach (var collection in this) + foreach (var collection in snapshot) { if (collection.GetOwnSettings(mod.Index)?.Settings.FixAll(mod) ?? false) _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); @@ -359,8 +402,9 @@ private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? gro type.HandlingInfo(out var requiresSaving, out _, out _); if (!requiresSaving) return; - - foreach (var collection in this) + + var snapshot = GetModSnapShot(); + foreach (var collection in snapshot) { if (collection.GetOwnSettings(mod.Index)?.HandleChanges(type, mod, group, option, movedToIdx) ?? false) _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); @@ -374,11 +418,22 @@ private void OnModFileChanged(Mod mod, FileRegistry file) if (file.CurrentUsage == 0) return; - foreach (var collection in this) + var snapshot = GetModSnapShot(); + foreach (var collection in snapshot) { var (settings, _) = collection.GetActualSettings(mod.Index); if (settings is { Enabled: true }) collection.Counters.IncrementChange(); } } + + private ModCollection[] GetModSnapShot() + { + ModCollection[] snapshot; + lock (_collectionsLock) + { + snapshot = _collections.Skip(1).ToArray(); + } + return snapshot; + } }