This commit is contained in:
Mayo66 2025-12-11 09:33:44 -08:00 committed by GitHub
commit 66282b73fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

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