diff --git a/Penumbra.Api b/Penumbra.Api index d87dfa44..6c6533ac 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit d87dfa44ff6efcf4fe576d8a877c78f4ac0dc893 +Subproject commit 6c6533ac60ee6e5e401bb9a65b31ad843d1757cd diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index da348667..da216702 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -12,14 +12,14 @@ using System.Numerics; using Dalamud.Utility; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; -using Penumbra.Collections; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI; - +using Penumbra.Collections.Manager; + namespace Penumbra.Api; public class IpcTester : IDisposable @@ -1213,7 +1213,7 @@ public class IpcTester : IDisposable DrawIntro(Ipc.CreateTemporaryCollection.Label, "Copy Existing Collection"); if (ImGuiUtil.DrawDisabledButton("Copy##Collection", Vector2.Zero, "Copies the effective list from the collection named in Temporary Mod Name...", - !Penumbra.CollectionManager.ByName(_tempModName, out var copyCollection)) + !Penumbra.CollectionManager.Storage.ByName(_tempModName, out var copyCollection)) && copyCollection is { HasCache: true }) { var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString()); diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index af3fec75..5e4c44a7 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -22,6 +22,7 @@ using Penumbra.Mods.Manager; using Penumbra.String; using Penumbra.String.Classes; using Penumbra.Services; +using Penumbra.Collections.Manager; namespace Penumbra.Api; @@ -59,7 +60,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return; CheckInitialized(); - _communicator.CreatingCharacterBase.Event += new Action(value); + _communicator.CreatingCharacterBase.Subscribe(new Action(value)); } remove { @@ -67,7 +68,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return; CheckInitialized(); - _communicator.CreatingCharacterBase.Event -= new Action(value); + _communicator.CreatingCharacterBase.Unsubscribe(new Action(value)); } } @@ -79,7 +80,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return; CheckInitialized(); - _communicator.CreatedCharacterBase.Event += new Action(value); + _communicator.CreatedCharacterBase.Subscribe(new Action(value)); } remove { @@ -87,7 +88,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return; CheckInitialized(); - _communicator.CreatedCharacterBase.Event -= new Action(value); + _communicator.CreatedCharacterBase.Unsubscribe(new Action(value)); } } @@ -129,12 +130,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi _lumina = (Lumina.GameData?)_dalamud.GameData.GetType() .GetField("gameData", BindingFlags.Instance | BindingFlags.NonPublic) ?.GetValue(_dalamud.GameData); - foreach (var collection in _collectionManager) + foreach (var collection in _collectionManager.Storage) SubscribeToCollection(collection); - _communicator.CollectionChange.Event += SubscribeToNewCollections; - _resourceLoader.ResourceLoaded += OnResourceLoaded; - _communicator.ModPathChanged.Event += ModPathChangeSubscriber; + _communicator.CollectionChange.Subscribe(SubscribeToNewCollections); + _resourceLoader.ResourceLoaded += OnResourceLoaded; + _communicator.ModPathChanged.Subscribe(ModPathChangeSubscriber); } public unsafe void Dispose() @@ -142,28 +143,28 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!Valid) return; - foreach (var collection in _collectionManager) + foreach (var collection in _collectionManager.Storage) { if (_delegates.TryGetValue(collection, out var del)) collection.ModSettingChanged -= del; } - _resourceLoader.ResourceLoaded -= OnResourceLoaded; - _communicator.CollectionChange.Event -= SubscribeToNewCollections; - _communicator.ModPathChanged.Event -= ModPathChangeSubscriber; - _lumina = null; - _communicator = null!; - _penumbra = null!; - _modManager = null!; - _resourceLoader = null!; - _config = null!; - _collectionManager = null!; - _dalamud = null!; - _tempCollections = null!; - _tempMods = null!; - _actors = null!; - _collectionResolver = null!; - _cutsceneService = null!; + _resourceLoader.ResourceLoaded -= OnResourceLoaded; + _communicator.CollectionChange.Unsubscribe(SubscribeToNewCollections); + _communicator.ModPathChanged.Unsubscribe(ModPathChangeSubscriber); + _lumina = null; + _communicator = null!; + _penumbra = null!; + _modManager = null!; + _resourceLoader = null!; + _config = null!; + _collectionManager = null!; + _dalamud = null!; + _tempCollections = null!; + _tempMods = null!; + _actors = null!; + _collectionResolver = null!; + _cutsceneService = null!; } public event ChangedItemClick? ChangedItemClicked; @@ -187,12 +188,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi add { CheckInitialized(); - _communicator.ModDirectoryChanged.Event += value; + _communicator.ModDirectoryChanged.Subscribe(value!); } remove { CheckInitialized(); - _communicator.ModDirectoryChanged.Event -= value; + _communicator.ModDirectoryChanged.Unsubscribe(value!); } } @@ -283,13 +284,13 @@ public class PenumbraApi : IDisposable, IPenumbraApi public string ResolveDefaultPath(string path) { CheckInitialized(); - return ResolvePath(path, _modManager, _collectionManager.Default); + return ResolvePath(path, _modManager, _collectionManager.Active.Default); } public string ResolveInterfacePath(string path) { CheckInitialized(); - return ResolvePath(path, _modManager, _collectionManager.Interface); + return ResolvePath(path, _modManager, _collectionManager.Active.Interface); } public string ResolvePlayerPath(string path) @@ -313,7 +314,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); return ResolvePath(path, _modManager, - _collectionManager.Individual(NameToIdentifier(characterName, worldId))); + _collectionManager.Active.Individual(NameToIdentifier(characterName, worldId))); } // TODO: cleanup when incrementing API level @@ -329,7 +330,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi path, }; - var ret = _collectionManager.Individual(NameToIdentifier(characterName, worldId)).ReverseResolvePath(new FullPath(path)); + var ret = _collectionManager.Active.Individual(NameToIdentifier(characterName, worldId)).ReverseResolvePath(new FullPath(path)); return ret.Select(r => r.ToString()).ToArray(); } @@ -386,7 +387,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi CheckInitialized(); try { - if (!_collectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.Storage.ByName(collectionName, out var collection)) collection = ModCollection.Empty; if (collection.HasCache) @@ -408,7 +409,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!Enum.IsDefined(type)) return string.Empty; - var collection = _collectionManager.ByType((CollectionType)type); + var collection = _collectionManager.Active.ByType((CollectionType)type); return collection?.Name ?? string.Empty; } @@ -419,7 +420,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!Enum.IsDefined(type)) return (PenumbraApiEc.InvalidArgument, string.Empty); - var oldCollection = _collectionManager.ByType((CollectionType)type)?.Name ?? string.Empty; + var oldCollection = _collectionManager.Active.ByType((CollectionType)type)?.Name ?? string.Empty; if (collectionName.Length == 0) { @@ -429,11 +430,11 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!allowDelete || type is ApiCollectionType.Current or ApiCollectionType.Default or ApiCollectionType.Interface) return (PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection); - _collectionManager.RemoveSpecialCollection((CollectionType)type); + _collectionManager.Active.RemoveSpecialCollection((CollectionType)type); return (PenumbraApiEc.Success, oldCollection); } - if (!_collectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.Storage.ByName(collectionName, out var collection)) return (PenumbraApiEc.CollectionMissing, oldCollection); if (oldCollection.Length == 0) @@ -441,14 +442,14 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!allowCreateNew) return (PenumbraApiEc.AssignmentCreationDisallowed, oldCollection); - _collectionManager.CreateSpecialCollection((CollectionType)type); + _collectionManager.Active.CreateSpecialCollection((CollectionType)type); } else if (oldCollection == collection.Name) { return (PenumbraApiEc.NothingChanged, oldCollection); } - _collectionManager.SetCollection(collection, (CollectionType)type); + _collectionManager.Active.SetCollection(collection, (CollectionType)type); return (PenumbraApiEc.Success, oldCollection); } @@ -457,9 +458,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi CheckInitialized(); var id = AssociatedIdentifier(gameObjectIdx); if (!id.IsValid) - return (false, false, _collectionManager.Default.Name); + return (false, false, _collectionManager.Active.Default.Name); - if (_collectionManager.Individuals.TryGetValue(id, out var collection)) + if (_collectionManager.Active.Individuals.TryGetValue(id, out var collection)) return (true, true, collection.Name); AssociatedCollection(gameObjectIdx, out collection); @@ -472,9 +473,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi CheckInitialized(); var id = AssociatedIdentifier(gameObjectIdx); if (!id.IsValid) - return (PenumbraApiEc.InvalidIdentifier, _collectionManager.Default.Name); + return (PenumbraApiEc.InvalidIdentifier, _collectionManager.Active.Default.Name); - var oldCollection = _collectionManager.Individuals.TryGetValue(id, out var c) ? c.Name : string.Empty; + var oldCollection = _collectionManager.Active.Individuals.TryGetValue(id, out var c) ? c.Name : string.Empty; if (collectionName.Length == 0) { @@ -484,12 +485,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!allowDelete) return (PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection); - var idx = _collectionManager.Individuals.Index(id); - _collectionManager.RemoveIndividualCollection(idx); + var idx = _collectionManager.Active.Individuals.Index(id); + _collectionManager.Active.RemoveIndividualCollection(idx); return (PenumbraApiEc.Success, oldCollection); } - if (!_collectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.Storage.ByName(collectionName, out var collection)) return (PenumbraApiEc.CollectionMissing, oldCollection); if (oldCollection.Length == 0) @@ -497,40 +498,40 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!allowCreateNew) return (PenumbraApiEc.AssignmentCreationDisallowed, oldCollection); - var ids = _collectionManager.Individuals.GetGroup(id); - _collectionManager.CreateIndividualCollection(ids); + var ids = _collectionManager.Active.Individuals.GetGroup(id); + _collectionManager.Active.CreateIndividualCollection(ids); } else if (oldCollection == collection.Name) { return (PenumbraApiEc.NothingChanged, oldCollection); } - _collectionManager.SetCollection(collection, CollectionType.Individual, _collectionManager.Individuals.Index(id)); + _collectionManager.Active.SetCollection(collection, CollectionType.Individual, _collectionManager.Active.Individuals.Index(id)); return (PenumbraApiEc.Success, oldCollection); } public IList GetCollections() { CheckInitialized(); - return _collectionManager.Select(c => c.Name).ToArray(); + return _collectionManager.Storage.Select(c => c.Name).ToArray(); } public string GetCurrentCollection() { CheckInitialized(); - return _collectionManager.Current.Name; + return _collectionManager.Active.Current.Name; } public string GetDefaultCollection() { CheckInitialized(); - return _collectionManager.Default.Name; + return _collectionManager.Active.Default.Name; } public string GetInterfaceCollection() { CheckInitialized(); - return _collectionManager.Interface.Name; + return _collectionManager.Active.Interface.Name; } // TODO: cleanup when incrementing API level @@ -540,9 +541,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi public (string, bool) GetCharacterCollection(string characterName, ushort worldId) { CheckInitialized(); - return _collectionManager.Individuals.TryGetCollection(NameToIdentifier(characterName, worldId), out var collection) + return _collectionManager.Active.Individuals.TryGetCollection(NameToIdentifier(characterName, worldId), out var collection) ? (collection.Name, true) - : (_collectionManager.Default.Name, false); + : (_collectionManager.Active.Default.Name, false); } public unsafe (nint, string) GetDrawObjectInfo(nint drawObject) @@ -576,7 +577,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi string modDirectory, string modName, bool allowInheritance) { CheckInitialized(); - if (!_collectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.Storage.ByName(collectionName, out var collection)) return (PenumbraApiEc.CollectionMissing, null); if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) @@ -679,7 +680,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc TryInheritMod(string collectionName, string modDirectory, string modName, bool inherit) { CheckInitialized(); - if (!_collectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.Storage.ByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) @@ -692,7 +693,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc TrySetMod(string collectionName, string modDirectory, string modName, bool enabled) { CheckInitialized(); - if (!_collectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.Storage.ByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) @@ -704,7 +705,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc TrySetModPriority(string collectionName, string modDirectory, string modName, int priority) { CheckInitialized(); - if (!_collectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.Storage.ByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) @@ -717,7 +718,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi string optionName) { CheckInitialized(); - if (!_collectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.Storage.ByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) @@ -740,7 +741,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi IReadOnlyList optionNames) { CheckInitialized(); - if (!_collectionManager.ByName(collectionName, out var collection)) + if (!_collectionManager.Storage.ByName(collectionName, out var collection)) return PenumbraApiEc.CollectionMissing; if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) @@ -788,9 +789,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi .FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryTo, StringComparison.OrdinalIgnoreCase))?.Index ?? -1; if (string.IsNullOrEmpty(collectionName)) - foreach (var collection in _collectionManager) + foreach (var collection in _collectionManager.Storage) collection.CopyModSettings(sourceModIdx, modDirectoryFrom, targetModIdx, modDirectoryTo); - else if (_collectionManager.ByName(collectionName, out var collection)) + else if (_collectionManager.Storage.ByName(collectionName, out var collection)) collection.CopyModSettings(sourceModIdx, modDirectoryFrom, targetModIdx, modDirectoryTo); else return PenumbraApiEc.CollectionMissing; @@ -809,7 +810,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if (!identifier.IsValid) return (PenumbraApiEc.InvalidArgument, string.Empty); - if (!forceOverwriteCharacter && _collectionManager.Individuals.ContainsKey(identifier) + if (!forceOverwriteCharacter && _collectionManager.Active.Individuals.ContainsKey(identifier) || _tempCollections.Collections.ContainsKey(identifier)) return (PenumbraApiEc.CharacterCollectionExists, string.Empty); @@ -859,7 +860,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi return PenumbraApiEc.AssignmentDeletionFailed; } else if (_tempCollections.Collections.ContainsKey(identifier) - || _collectionManager.Individuals.ContainsKey(identifier)) + || _collectionManager.Active.Individuals.ContainsKey(identifier)) { return PenumbraApiEc.CharacterCollectionExists; } @@ -907,7 +908,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); if (!_tempCollections.CollectionByName(collectionName, out var collection) - && !_collectionManager.ByName(collectionName, out collection)) + && !_collectionManager.Storage.ByName(collectionName, out collection)) return PenumbraApiEc.CollectionMissing; if (!ConvertPaths(paths, out var p)) @@ -938,7 +939,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi { CheckInitialized(); if (!_tempCollections.CollectionByName(collectionName, out var collection) - && !_collectionManager.ByName(collectionName, out collection)) + && !_collectionManager.Storage.ByName(collectionName, out collection)) return PenumbraApiEc.CollectionMissing; return _tempMods.Unregister(tag, collection, priority) switch @@ -967,7 +968,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi var identifier = NameToIdentifier(characterName, worldId); var collection = _tempCollections.Collections.TryGetCollection(identifier, out var c) ? c - : _collectionManager.Individual(identifier); + : _collectionManager.Active.Individual(identifier); var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty(); return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion); } @@ -1002,7 +1003,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private unsafe bool AssociatedCollection(int gameObjectIdx, out ModCollection collection) { - collection = _collectionManager.Default; + collection = _collectionManager.Active.Default; if (gameObjectIdx < 0 || gameObjectIdx >= _dalamud.Objects.Length) return false; diff --git a/Penumbra/Api/TempModManager.cs b/Penumbra/Api/TempModManager.cs index 07e65c36..747d49cd 100644 --- a/Penumbra/Api/TempModManager.cs +++ b/Penumbra/Api/TempModManager.cs @@ -5,7 +5,8 @@ using Penumbra.Mods; using System.Collections.Generic; using Penumbra.Services; using Penumbra.String.Classes; - +using Penumbra.Collections.Manager; + namespace Penumbra.Api; public enum RedirectResult @@ -26,12 +27,12 @@ public class TempModManager : IDisposable public TempModManager(CommunicatorService communicator) { _communicator = communicator; - _communicator.CollectionChange.Event += OnCollectionChange; + _communicator.CollectionChange.Subscribe(OnCollectionChange); } public void Dispose() { - _communicator.CollectionChange.Event -= OnCollectionChange; + _communicator.CollectionChange.Unsubscribe(OnCollectionChange); } public IReadOnlyDictionary> Mods diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs deleted file mode 100644 index 50bbefea..00000000 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ /dev/null @@ -1,419 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using OtterGui; -using Penumbra.Mods; -using Penumbra.UI; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Dalamud.Interface.Internal.Notifications; -using Penumbra.GameData.Actors; -using Penumbra.Services; -using Penumbra.Util; - -namespace Penumbra.Collections; - -public sealed partial class CollectionManager : ISavable -{ - public const int Version = 1; - - // The collection currently selected for changing settings. - public ModCollection Current { get; private set; } = ModCollection.Empty; - - // The collection currently selected is in use either as an active collection or through inheritance. - public bool CurrentCollectionInUse { get; private set; } - - // The collection used for general file redirections and all characters not specifically named. - public ModCollection Default { get; private set; } = ModCollection.Empty; - - // The collection used for all files categorized as UI files. - public ModCollection Interface { get; private set; } = ModCollection.Empty; - - // A single collection that can not be deleted as a fallback for the current collection. - private ModCollection DefaultName { get; set; } = ModCollection.Empty; - - // The list of character collections. - public readonly IndividualCollections Individuals; - - public ModCollection Individual(ActorIdentifier identifier) - => Individuals.TryGetCollection(identifier, out var c) ? c : Default; - - // Special Collections - private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues().Length - 3]; - - // Return the configured collection for the given type or null. - // Does not handle Inactive, use ByName instead. - public ModCollection? ByType(CollectionType type) - => ByType(type, ActorIdentifier.Invalid); - - public ModCollection? ByType(CollectionType type, ActorIdentifier identifier) - { - if (type.IsSpecial()) - return _specialCollections[(int)type]; - - return type switch - { - CollectionType.Default => Default, - CollectionType.Interface => Interface, - CollectionType.Current => Current, - CollectionType.Individual => identifier.IsValid && Individuals.TryGetValue(identifier, out var c) ? c : null, - _ => null, - }; - } - - // Set a active collection, can be used to set Default, Current, Interface, Special, or Individual collections. - private void SetCollection(int newIdx, CollectionType collectionType, int individualIndex = -1) - { - var oldCollectionIdx = collectionType switch - { - CollectionType.Default => Default.Index, - CollectionType.Interface => Interface.Index, - CollectionType.Current => Current.Index, - CollectionType.Individual => individualIndex < 0 || individualIndex >= Individuals.Count - ? -1 - : Individuals[individualIndex].Collection.Index, - _ when collectionType.IsSpecial() => _specialCollections[(int)collectionType]?.Index ?? Default.Index, - _ => -1, - }; - - if (oldCollectionIdx == -1 || newIdx == oldCollectionIdx) - return; - - var newCollection = this[newIdx]; - if (newIdx > ModCollection.Empty.Index) - newCollection.CreateCache(collectionType is CollectionType.Default); - - switch (collectionType) - { - case CollectionType.Default: - Default = newCollection; - break; - case CollectionType.Interface: - Interface = newCollection; - break; - case CollectionType.Current: - Current = newCollection; - break; - case CollectionType.Individual: - if (!Individuals.ChangeCollection(individualIndex, newCollection)) - { - RemoveCache(newIdx); - return; - } - - break; - default: - _specialCollections[(int)collectionType] = newCollection; - break; - } - - RemoveCache(oldCollectionIdx); - - UpdateCurrentCollectionInUse(); - _communicator.CollectionChange.Invoke(collectionType, this[oldCollectionIdx], newCollection, - collectionType == CollectionType.Individual ? Individuals[individualIndex].DisplayName : string.Empty); - } - - private void UpdateCurrentCollectionInUse() - => CurrentCollectionInUse = _specialCollections - .OfType() - .Prepend(Interface) - .Prepend(Default) - .Concat(Individuals.Assignments.Select(kvp => kvp.Collection)) - .SelectMany(c => c.GetFlattenedInheritance()).Contains(Current); - - public void SetCollection(ModCollection collection, CollectionType collectionType, int individualIndex = -1) - => SetCollection(collection.Index, collectionType, individualIndex); - - // Create a special collection if it does not exist and set it to Empty. - public bool CreateSpecialCollection(CollectionType collectionType) - { - if (!collectionType.IsSpecial() || _specialCollections[(int)collectionType] != null) - return false; - - _specialCollections[(int)collectionType] = Default; - _communicator.CollectionChange.Invoke(collectionType, null, Default, string.Empty); - return true; - } - - // Remove a special collection if it exists - public void RemoveSpecialCollection(CollectionType collectionType) - { - if (!collectionType.IsSpecial()) - return; - - var old = _specialCollections[(int)collectionType]; - if (old != null) - { - _specialCollections[(int)collectionType] = null; - _communicator.CollectionChange.Invoke(collectionType, old, null, string.Empty); - } - } - - // Wrappers around Individual Collection handling. - public void CreateIndividualCollection(params ActorIdentifier[] identifiers) - { - if (Individuals.Add(identifiers, Default)) - _communicator.CollectionChange.Invoke(CollectionType.Individual, null, Default, Individuals.Last().DisplayName); - } - - public void RemoveIndividualCollection(int individualIndex) - { - if (individualIndex < 0 || individualIndex >= Individuals.Count) - return; - - var (name, old) = Individuals[individualIndex]; - if (Individuals.Delete(individualIndex)) - _communicator.CollectionChange.Invoke(CollectionType.Individual, old, null, name); - } - - public void MoveIndividualCollection(int from, int to) - { - if (Individuals.Move(from, to)) - Penumbra.SaveService.QueueSave(this); - } - - // Obtain the index of a collection by name. - private int GetIndexForCollectionName(string name) - => name.Length == 0 ? ModCollection.Empty.Index : _collections.IndexOf(c => c.Name == name); - - // Load default, current, special, and character collections from config. - // Then create caches. If a collection does not exist anymore, reset it to an appropriate default. - private void LoadCollections(FilenameService files) - { - var configChanged = !ReadActiveCollections(files, out var jObject); - - // Load the default collection. - var defaultName = jObject[nameof(Default)]?.ToObject() ?? (configChanged ? ModCollection.DefaultCollection : ModCollection.Empty.Name); - var defaultIdx = GetIndexForCollectionName(defaultName); - if (defaultIdx < 0) - { - Penumbra.ChatService.NotificationMessage( - $"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Name}.", "Load Failure", - NotificationType.Warning); - Default = ModCollection.Empty; - configChanged = true; - } - else - { - Default = this[defaultIdx]; - } - - // Load the interface collection. - var interfaceName = jObject[nameof(Interface)]?.ToObject() ?? Default.Name; - var interfaceIdx = GetIndexForCollectionName(interfaceName); - if (interfaceIdx < 0) - { - Penumbra.ChatService.NotificationMessage( - $"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Name}.", - "Load Failure", NotificationType.Warning); - Interface = ModCollection.Empty; - configChanged = true; - } - else - { - Interface = this[interfaceIdx]; - } - - // Load the current collection. - var currentName = jObject[nameof(Current)]?.ToObject() ?? ModCollection.DefaultCollection; - var currentIdx = GetIndexForCollectionName(currentName); - if (currentIdx < 0) - { - Penumbra.ChatService.NotificationMessage( - $"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollection.DefaultCollection}.", - "Load Failure", NotificationType.Warning); - Current = DefaultName; - configChanged = true; - } - else - { - Current = this[currentIdx]; - } - - // Load special collections. - foreach (var (type, name, _) in CollectionTypeExtensions.Special) - { - var typeName = jObject[type.ToString()]?.ToObject(); - if (typeName != null) - { - var idx = GetIndexForCollectionName(typeName); - if (idx < 0) - { - Penumbra.ChatService.NotificationMessage($"Last choice of {name} Collection {typeName} is not available, removed.", - "Load Failure", - NotificationType.Warning); - configChanged = true; - } - else - { - _specialCollections[(int)type] = this[idx]; - } - } - } - - configChanged |= MigrateIndividualCollections(jObject); - configChanged |= Individuals.ReadJObject(jObject[nameof(Individuals)] as JArray, this); - - // Save any changes and create all required caches. - if (configChanged) - Penumbra.SaveService.ImmediateSave(this); - } - - // Migrate ungendered collections to Male and Female for 0.5.9.0. - public static void MigrateUngenderedCollections(FilenameService fileNames) - { - if (!ReadActiveCollections(fileNames, out var jObject)) - return; - - foreach (var (type, _, _) in CollectionTypeExtensions.Special.Where(t => t.Item2.StartsWith("Male "))) - { - var oldName = type.ToString()[4..]; - var value = jObject[oldName]; - if (value == null) - continue; - - jObject.Remove(oldName); - jObject.Add("Male" + oldName, value); - jObject.Add("Female" + oldName, value); - } - - using var stream = File.Open(fileNames.ActiveCollectionsFile, FileMode.Truncate); - using var writer = new StreamWriter(stream); - using var j = new JsonTextWriter(writer); - j.Formatting = Formatting.Indented; - jObject.WriteTo(j); - } - - // Migrate individual collections to Identifiers for 0.6.0. - private bool MigrateIndividualCollections(JObject jObject) - { - var version = jObject[nameof(Version)]?.Value() ?? 0; - if (version > 0) - return false; - - // Load character collections. If a player name comes up multiple times, the last one is applied. - var characters = jObject["Characters"]?.ToObject>() ?? new Dictionary(); - var dict = new Dictionary(characters.Count); - foreach (var (player, collectionName) in characters) - { - var idx = GetIndexForCollectionName(collectionName); - if (idx < 0) - { - Penumbra.ChatService.NotificationMessage( - $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {ModCollection.Empty.Name}.", "Load Failure", - NotificationType.Warning); - dict.Add(player, ModCollection.Empty); - } - else - { - dict.Add(player, this[idx]); - } - } - - Individuals.Migrate0To1(dict); - return true; - } - - // Read the active collection file into a jObject. - // Returns true if this is successful, false if the file does not exist or it is unsuccessful. - private static bool ReadActiveCollections(FilenameService files, out JObject ret) - { - var file = files.ActiveCollectionsFile; - if (File.Exists(file)) - try - { - ret = JObject.Parse(File.ReadAllText(file)); - return true; - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not read active collections from file {file}:\n{e}"); - } - - ret = new JObject(); - return false; - } - - // Save if any of the active collections is changed. - private void SaveOnChange(CollectionType collectionType, ModCollection? _1, ModCollection? _2, string _3) - { - if (collectionType is not CollectionType.Inactive and not CollectionType.Temporary) - Penumbra.SaveService.QueueSave(this); - } - - // Cache handling. Usually recreate caches on the next framework tick, - // but at launch create all of them at once. - public void CreateNecessaryCaches() - { - var tasks = _specialCollections.OfType() - .Concat(Individuals.Select(p => p.Collection)) - .Prepend(Current) - .Prepend(Default) - .Prepend(Interface) - .Distinct() - .Select(c => Task.Run(() => c.CalculateEffectiveFileListInternal(c == Default))) - .ToArray(); - - Task.WaitAll(tasks); - } - - private void RemoveCache(int idx) - { - if (idx != ModCollection.Empty.Index - && idx != Default.Index - && idx != Interface.Index - && idx != Current.Index - && _specialCollections.All(c => c == null || c.Index != idx) - && Individuals.Select(p => p.Collection).All(c => c.Index != idx)) - _collections[idx].ClearCache(); - } - - // Recalculate effective files for active collections on events. - private void OnModAddedActive(Mod mod) - { - foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) - collection._cache!.AddMod(mod, true); - } - - private void OnModRemovedActive(Mod mod) - { - foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) - collection._cache!.RemoveMod(mod, true); - } - - private void OnModMovedActive(Mod mod) - { - foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) - collection._cache!.ReloadMod(mod, true); - } - - public string ToFilename(FilenameService fileNames) - => fileNames.ActiveCollectionsFile; - - public string TypeName - => "Active Collections"; - - public string LogName(string _) - => "to file"; - - public void Save(StreamWriter writer) - { - var jObj = new JObject - { - { nameof(Version), Version }, - { nameof(Default), Default.Name }, - { nameof(Interface), Interface.Name }, - { nameof(Current), Current.Name }, - }; - foreach (var (type, collection) in _specialCollections.WithIndex().Where(p => p.Value != null) - .Select(p => ((CollectionType)p.Index, p.Value!))) - jObj.Add(type.ToString(), collection.Name); - - jObj.Add(nameof(Individuals), Individuals.ToJObject()); - using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; - jObj.WriteTo(j); - } -} \ No newline at end of file diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs deleted file mode 100644 index d3628c27..00000000 --- a/Penumbra/Collections/CollectionManager.cs +++ /dev/null @@ -1,480 +0,0 @@ -using OtterGui; -using OtterGui.Filesystem; -using Penumbra.Mods; -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using Penumbra.Api; -using Penumbra.GameData.Actors; -using Penumbra.Interop.Services; -using Penumbra.Mods.Manager; -using Penumbra.Services; -using Penumbra.Util; -using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; - -namespace Penumbra.Collections; - -public sealed partial class CollectionManager : IDisposable, IEnumerable -{ - private readonly ModManager _modManager; - private readonly CommunicatorService _communicator; - private readonly SaveService _saveService; - private readonly CharacterUtility _characterUtility; - private readonly ResidentResourceManager _residentResources; - private readonly Configuration _config; - - - // The empty collection is always available and always has index 0. - // It can not be deleted or moved. - private readonly List _collections = new() - { - ModCollection.Empty, - }; - - public ModCollection this[Index idx] - => _collections[idx]; - - public ModCollection? this[string name] - => ByName(name, out var c) ? c : null; - - public int Count - => _collections.Count; - - // Obtain a collection case-independently by name. - public bool ByName(string name, [NotNullWhen(true)] out ModCollection? collection) - => _collections.FindFirst(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase), out collection); - - // Default enumeration skips the empty collection. - public IEnumerator GetEnumerator() - => _collections.Skip(1).GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public IEnumerable GetEnumeratorWithEmpty() - => _collections; - - public CollectionManager(StartTracker timer, CommunicatorService communicator, FilenameService files, CharacterUtility characterUtility, - ResidentResourceManager residentResources, Configuration config, ModManager modManager, IndividualCollections individuals, - SaveService saveService) - { - using var time = timer.Measure(StartTimeType.Collections); - _communicator = communicator; - _characterUtility = characterUtility; - _residentResources = residentResources; - _config = config; - _modManager = modManager; - _saveService = saveService; - Individuals = individuals; - - // The collection manager reacts to changes in mods by itself. - _communicator.ModDiscoveryStarted.Event += OnModDiscoveryStarted; - _communicator.ModDiscoveryFinished.Event += OnModDiscoveryFinished; - _communicator.ModOptionChanged.Event += OnModOptionsChanged; - _communicator.ModPathChanged.Event += OnModPathChange; - _communicator.CollectionChange.Event += SaveOnChange; - _communicator.TemporaryGlobalModChange.Event += OnGlobalModChange; - ReadCollections(files); - LoadCollections(files); - UpdateCurrentCollectionInUse(); - CreateNecessaryCaches(); - } - - public void Dispose() - { - _communicator.CollectionChange.Event -= SaveOnChange; - _communicator.TemporaryGlobalModChange.Event -= OnGlobalModChange; - _communicator.ModDiscoveryStarted.Event -= OnModDiscoveryStarted; - _communicator.ModDiscoveryFinished.Event -= OnModDiscoveryFinished; - _communicator.ModOptionChanged.Event -= OnModOptionsChanged; - _communicator.ModPathChanged.Event -= OnModPathChange; - } - - private void OnGlobalModChange(TemporaryMod mod, bool created, bool removed) - => TempModManager.OnGlobalModChange(_collections, mod, created, removed); - - // Returns true if the name is not empty, it is not the name of the empty collection - // and no existing collection results in the same filename as name. - public bool CanAddCollection(string name, out string fixedName) - { - if (!ModCollection.IsValidName(name)) - { - fixedName = string.Empty; - return false; - } - - name = name.RemoveInvalidPathSymbols().ToLowerInvariant(); - if (name.Length == 0 - || name == ModCollection.Empty.Name.ToLowerInvariant() - || _collections.Any(c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == name)) - { - fixedName = string.Empty; - return false; - } - - fixedName = name; - return true; - } - - // Add a new collection of the given name. - // If duplicate is not-null, the new collection will be a duplicate of it. - // If the name of the collection would result in an already existing filename, skip it. - // Returns true if the collection was successfully created and fires a Inactive event. - // Also sets the current collection to the new collection afterwards. - public bool AddCollection(string name, ModCollection? duplicate) - { - if (!CanAddCollection(name, out var fixedName)) - { - Penumbra.Log.Warning($"The new collection {name} would lead to the same path {fixedName} as one that already exists."); - return false; - } - - var newCollection = duplicate?.Duplicate(name) ?? ModCollection.CreateNewEmpty(name); - newCollection.Index = _collections.Count; - _collections.Add(newCollection); - - Penumbra.SaveService.ImmediateSave(newCollection); - Penumbra.Log.Debug($"Added collection {newCollection.AnonymizedName}."); - _communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty); - SetCollection(newCollection.Index, CollectionType.Current); - return true; - } - - // Remove the given collection if it exists and is neither the empty nor the default-named collection. - // If the removed collection was active, it also sets the corresponding collection to the appropriate default. - // Also removes the collection from inheritances of all other collections. - public bool RemoveCollection(int idx) - { - if (idx <= ModCollection.Empty.Index || idx >= _collections.Count) - { - Penumbra.Log.Error("Can not remove the empty collection."); - return false; - } - - if (idx == DefaultName.Index) - { - Penumbra.Log.Error("Can not remove the default collection."); - return false; - } - - if (idx == Current.Index) - SetCollection(DefaultName.Index, CollectionType.Current); - - if (idx == Default.Index) - SetCollection(ModCollection.Empty.Index, CollectionType.Default); - - for (var i = 0; i < _specialCollections.Length; ++i) - { - if (idx == _specialCollections[i]?.Index) - SetCollection(ModCollection.Empty, (CollectionType)i); - } - - for (var i = 0; i < Individuals.Count; ++i) - { - if (Individuals[i].Collection.Index == idx) - SetCollection(ModCollection.Empty, CollectionType.Individual, i); - } - - var collection = _collections[idx]; - - // Clear own inheritances. - foreach (var inheritance in collection.Inheritance) - collection.ClearSubscriptions(inheritance); - - Penumbra.SaveService.ImmediateDelete(collection); - _collections.RemoveAt(idx); - - // Clear external inheritances. - foreach (var c in _collections) - { - var inheritedIdx = c._inheritance.IndexOf(collection); - if (inheritedIdx >= 0) - c.RemoveInheritance(inheritedIdx); - - if (c.Index > idx) - --c.Index; - } - - Penumbra.Log.Debug($"Removed collection {collection.AnonymizedName}."); - _communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty); - return true; - } - - public bool RemoveCollection(ModCollection collection) - => RemoveCollection(collection.Index); - - private void OnModDiscoveryStarted() - { - foreach (var collection in this) - collection.PrepareModDiscovery(); - } - - private void OnModDiscoveryFinished() - { - // First, re-apply all mod settings. - foreach (var collection in this) - collection.ApplyModSettings(); - - // Afterwards, we update the caches. This can not happen in the same loop due to inheritance. - foreach (var collection in this.Where(c => c.HasCache)) - collection.ForceCacheUpdate(); - } - - - // A changed mod path forces changes for all collections, active and inactive. - private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, - DirectoryInfo? newDirectory) - { - switch (type) - { - case ModPathChangeType.Added: - foreach (var collection in this) - collection.AddMod(mod); - - OnModAddedActive(mod); - break; - case ModPathChangeType.Deleted: - OnModRemovedActive(mod); - foreach (var collection in this) - collection.RemoveMod(mod, mod.Index); - - break; - case ModPathChangeType.Moved: - OnModMovedActive(mod); - foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null)) - Penumbra.SaveService.QueueSave(collection); - - break; - case ModPathChangeType.StartingReload: - OnModRemovedActive(mod); - break; - case ModPathChangeType.Reloaded: - OnModAddedActive(mod); - break; - default: throw new ArgumentOutOfRangeException(nameof(type), type, null); - } - } - - // Automatically update all relevant collections when a mod is changed. - // This means saving if options change in a way where the settings may change and the collection has settings for this mod. - // And also updating effective file and meta manipulation lists if necessary. - private void OnModOptionsChanged(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx) - { - // Handle changes that break revertability. - if (type == ModOptionChangeType.PrepareChange) - { - foreach (var collection in this.Where(c => c.HasCache)) - { - if (collection[mod.Index].Settings is { Enabled: true }) - collection._cache!.RemoveMod(mod, false); - } - - return; - } - - type.HandlingInfo(out var requiresSaving, out var recomputeList, out var reload); - - // Handle changes that require overwriting the collection. - if (requiresSaving) - foreach (var collection in this) - { - if (collection._settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false) - _saveService.QueueSave(collection); - } - - // Handle changes that reload the mod if the changes did not need to be prepared, - // or re-add the mod if they were prepared. - if (recomputeList) - foreach (var collection in this.Where(c => c.HasCache)) - { - if (collection[mod.Index].Settings is { Enabled: true }) - { - if (reload) - collection._cache!.ReloadMod(mod, true); - else - collection._cache!.AddMod(mod, true); - } - } - } - - // Add the collection with the default name if it does not exist. - // It should always be ensured that it exists, otherwise it will be created. - // This can also not be deleted, so there are always at least the empty and a collection with default name. - private void AddDefaultCollection() - { - var idx = GetIndexForCollectionName(ModCollection.DefaultCollection); - if (idx >= 0) - { - DefaultName = this[idx]; - return; - } - - var defaultCollection = ModCollection.CreateNewEmpty((string)ModCollection.DefaultCollection); - _saveService.ImmediateSave(defaultCollection); - defaultCollection.Index = _collections.Count; - _collections.Add(defaultCollection); - } - - // Inheritances can not be setup before all collections are read, - // so this happens after reading the collections. - private void ApplyInheritances(IEnumerable> inheritances) - { - foreach (var (collection, inheritance) in this.Zip(inheritances)) - { - var changes = false; - foreach (var subCollectionName in inheritance) - { - if (!ByName(subCollectionName, out var subCollection)) - { - changes = true; - Penumbra.Log.Warning($"Inherited collection {subCollectionName} for {collection.Name} does not exist, removed."); - } - else if (!collection.AddInheritance(subCollection, false)) - { - changes = true; - Penumbra.Log.Warning($"{collection.Name} can not inherit from {subCollectionName}, removed."); - } - } - - if (changes) - _saveService.ImmediateSave(collection); - } - } - - // Read all collection files in the Collection Directory. - // Ensure that the default named collection exists, and apply inheritances afterwards. - // Duplicate collection files are not deleted, just not added here. - private void ReadCollections(FilenameService files) - { - var inheritances = new List>(); - foreach (var file in files.CollectionFiles) - { - var collection = ModCollection.LoadFromFile(file, out var inheritance); - if (collection == null || collection.Name.Length == 0) - continue; - - if (file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json") - Penumbra.Log.Warning($"Collection {file.Name} does not correspond to {collection.Name}."); - - if (this[collection.Name] != null) - { - Penumbra.Log.Warning($"Duplicate collection found: {collection.Name} already exists."); - } - else - { - inheritances.Add(inheritance); - collection.Index = _collections.Count; - _collections.Add(collection); - } - } - - AddDefaultCollection(); - ApplyInheritances(inheritances); - } - - public string RedundancyCheck(CollectionType type, ActorIdentifier id) - { - var checkAssignment = ByType(type, id); - if (checkAssignment == null) - return string.Empty; - - switch (type) - { - // Check individual assignments. We can only be sure of redundancy for world-overlap or ownership overlap. - case CollectionType.Individual: - switch (id.Type) - { - case IdentifierType.Player when id.HomeWorld != ushort.MaxValue: - { - var global = ByType(CollectionType.Individual, Penumbra.Actors.CreatePlayer(id.PlayerName, ushort.MaxValue)); - return global?.Index == checkAssignment.Index - ? "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it." - : string.Empty; - } - case IdentifierType.Owned: - if (id.HomeWorld != ushort.MaxValue) - { - var global = ByType(CollectionType.Individual, - Penumbra.Actors.CreateOwned(id.PlayerName, ushort.MaxValue, id.Kind, id.DataId)); - if (global?.Index == checkAssignment.Index) - return "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it."; - } - - var unowned = ByType(CollectionType.Individual, Penumbra.Actors.CreateNpc(id.Kind, id.DataId)); - return unowned?.Index == checkAssignment.Index - ? "Assignment is redundant due to an identical unowned NPC assignment existing.\nYou can remove it." - : string.Empty; - } - - break; - // The group of all Characters is redundant if they are all equal to Default or unassigned. - case CollectionType.MalePlayerCharacter: - case CollectionType.MaleNonPlayerCharacter: - case CollectionType.FemalePlayerCharacter: - case CollectionType.FemaleNonPlayerCharacter: - var first = ByType(CollectionType.MalePlayerCharacter) ?? Default; - var second = ByType(CollectionType.MaleNonPlayerCharacter) ?? Default; - var third = ByType(CollectionType.FemalePlayerCharacter) ?? Default; - var fourth = ByType(CollectionType.FemaleNonPlayerCharacter) ?? Default; - if (first.Index == second.Index - && first.Index == third.Index - && first.Index == fourth.Index - && first.Index == Default.Index) - return - "Assignment is currently redundant due to the group [Male, Female, Player, NPC] Characters being unassigned or identical to each other and Default.\n" - + "You can keep just the Default Assignment."; - - break; - // Children and Elderly are redundant if they are identical to both Male NPCs and Female NPCs, or if they are unassigned to Default. - case CollectionType.NonPlayerChild: - case CollectionType.NonPlayerElderly: - var maleNpc = ByType(CollectionType.MaleNonPlayerCharacter); - var femaleNpc = ByType(CollectionType.FemaleNonPlayerCharacter); - var collection1 = CollectionType.MaleNonPlayerCharacter; - var collection2 = CollectionType.FemaleNonPlayerCharacter; - if (maleNpc == null) - { - maleNpc = Default; - if (maleNpc.Index != checkAssignment.Index) - return string.Empty; - - collection1 = CollectionType.Default; - } - - if (femaleNpc == null) - { - femaleNpc = Default; - if (femaleNpc.Index != checkAssignment.Index) - return string.Empty; - - collection2 = CollectionType.Default; - } - - return collection1 == collection2 - ? $"Assignment is currently redundant due to overwriting {collection1.ToName()} with an identical collection.\nYou can remove them." - : $"Assignment is currently redundant due to overwriting {collection1.ToName()} and {collection2.ToName()} with an identical collection.\nYou can remove them."; - - // For other assignments, check the inheritance order, unassigned means fall-through, - // assigned needs identical assignments to be redundant. - default: - var group = type.InheritanceOrder(); - foreach (var parentType in group) - { - var assignment = ByType(parentType); - if (assignment == null) - continue; - - if (assignment.Index == checkAssignment.Index) - return - $"Assignment is currently redundant due to overwriting {parentType.ToName()} with an identical collection.\nYou can remove it."; - } - - break; - } - - return string.Empty; - } -} diff --git a/Penumbra/Collections/CollectionType.cs b/Penumbra/Collections/CollectionType.cs deleted file mode 100644 index 91fcc5a1..00000000 --- a/Penumbra/Collections/CollectionType.cs +++ /dev/null @@ -1,584 +0,0 @@ -using Penumbra.GameData.Enums; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection.Metadata.Ecma335; - -namespace Penumbra.Collections; - -public enum CollectionType : byte -{ - // Special Collections - Yourself = Api.Enums.ApiCollectionType.Yourself, - - MalePlayerCharacter = Api.Enums.ApiCollectionType.MalePlayerCharacter, - FemalePlayerCharacter = Api.Enums.ApiCollectionType.FemalePlayerCharacter, - MaleNonPlayerCharacter = Api.Enums.ApiCollectionType.MaleNonPlayerCharacter, - FemaleNonPlayerCharacter = Api.Enums.ApiCollectionType.FemaleNonPlayerCharacter, - NonPlayerChild = Api.Enums.ApiCollectionType.NonPlayerChild, - NonPlayerElderly = Api.Enums.ApiCollectionType.NonPlayerElderly, - - MaleMidlander = Api.Enums.ApiCollectionType.MaleMidlander, - FemaleMidlander = Api.Enums.ApiCollectionType.FemaleMidlander, - MaleHighlander = Api.Enums.ApiCollectionType.MaleHighlander, - FemaleHighlander = Api.Enums.ApiCollectionType.FemaleHighlander, - - MaleWildwood = Api.Enums.ApiCollectionType.MaleWildwood, - FemaleWildwood = Api.Enums.ApiCollectionType.FemaleWildwood, - MaleDuskwight = Api.Enums.ApiCollectionType.MaleDuskwight, - FemaleDuskwight = Api.Enums.ApiCollectionType.FemaleDuskwight, - - MalePlainsfolk = Api.Enums.ApiCollectionType.MalePlainsfolk, - FemalePlainsfolk = Api.Enums.ApiCollectionType.FemalePlainsfolk, - MaleDunesfolk = Api.Enums.ApiCollectionType.MaleDunesfolk, - FemaleDunesfolk = Api.Enums.ApiCollectionType.FemaleDunesfolk, - - MaleSeekerOfTheSun = Api.Enums.ApiCollectionType.MaleSeekerOfTheSun, - FemaleSeekerOfTheSun = Api.Enums.ApiCollectionType.FemaleSeekerOfTheSun, - MaleKeeperOfTheMoon = Api.Enums.ApiCollectionType.MaleKeeperOfTheMoon, - FemaleKeeperOfTheMoon = Api.Enums.ApiCollectionType.FemaleKeeperOfTheMoon, - - MaleSeawolf = Api.Enums.ApiCollectionType.MaleSeawolf, - FemaleSeawolf = Api.Enums.ApiCollectionType.FemaleSeawolf, - MaleHellsguard = Api.Enums.ApiCollectionType.MaleHellsguard, - FemaleHellsguard = Api.Enums.ApiCollectionType.FemaleHellsguard, - - MaleRaen = Api.Enums.ApiCollectionType.MaleRaen, - FemaleRaen = Api.Enums.ApiCollectionType.FemaleRaen, - MaleXaela = Api.Enums.ApiCollectionType.MaleXaela, - FemaleXaela = Api.Enums.ApiCollectionType.FemaleXaela, - - MaleHelion = Api.Enums.ApiCollectionType.MaleHelion, - FemaleHelion = Api.Enums.ApiCollectionType.FemaleHelion, - MaleLost = Api.Enums.ApiCollectionType.MaleLost, - FemaleLost = Api.Enums.ApiCollectionType.FemaleLost, - - MaleRava = Api.Enums.ApiCollectionType.MaleRava, - FemaleRava = Api.Enums.ApiCollectionType.FemaleRava, - MaleVeena = Api.Enums.ApiCollectionType.MaleVeena, - FemaleVeena = Api.Enums.ApiCollectionType.FemaleVeena, - - MaleMidlanderNpc = Api.Enums.ApiCollectionType.MaleMidlanderNpc, - FemaleMidlanderNpc = Api.Enums.ApiCollectionType.FemaleMidlanderNpc, - MaleHighlanderNpc = Api.Enums.ApiCollectionType.MaleHighlanderNpc, - FemaleHighlanderNpc = Api.Enums.ApiCollectionType.FemaleHighlanderNpc, - - MaleWildwoodNpc = Api.Enums.ApiCollectionType.MaleWildwoodNpc, - FemaleWildwoodNpc = Api.Enums.ApiCollectionType.FemaleWildwoodNpc, - MaleDuskwightNpc = Api.Enums.ApiCollectionType.MaleDuskwightNpc, - FemaleDuskwightNpc = Api.Enums.ApiCollectionType.FemaleDuskwightNpc, - - MalePlainsfolkNpc = Api.Enums.ApiCollectionType.MalePlainsfolkNpc, - FemalePlainsfolkNpc = Api.Enums.ApiCollectionType.FemalePlainsfolkNpc, - MaleDunesfolkNpc = Api.Enums.ApiCollectionType.MaleDunesfolkNpc, - FemaleDunesfolkNpc = Api.Enums.ApiCollectionType.FemaleDunesfolkNpc, - - MaleSeekerOfTheSunNpc = Api.Enums.ApiCollectionType.MaleSeekerOfTheSunNpc, - FemaleSeekerOfTheSunNpc = Api.Enums.ApiCollectionType.FemaleSeekerOfTheSunNpc, - MaleKeeperOfTheMoonNpc = Api.Enums.ApiCollectionType.MaleKeeperOfTheMoonNpc, - FemaleKeeperOfTheMoonNpc = Api.Enums.ApiCollectionType.FemaleKeeperOfTheMoonNpc, - - MaleSeawolfNpc = Api.Enums.ApiCollectionType.MaleSeawolfNpc, - FemaleSeawolfNpc = Api.Enums.ApiCollectionType.FemaleSeawolfNpc, - MaleHellsguardNpc = Api.Enums.ApiCollectionType.MaleHellsguardNpc, - FemaleHellsguardNpc = Api.Enums.ApiCollectionType.FemaleHellsguardNpc, - - MaleRaenNpc = Api.Enums.ApiCollectionType.MaleRaenNpc, - FemaleRaenNpc = Api.Enums.ApiCollectionType.FemaleRaenNpc, - MaleXaelaNpc = Api.Enums.ApiCollectionType.MaleXaelaNpc, - FemaleXaelaNpc = Api.Enums.ApiCollectionType.FemaleXaelaNpc, - - MaleHelionNpc = Api.Enums.ApiCollectionType.MaleHelionNpc, - FemaleHelionNpc = Api.Enums.ApiCollectionType.FemaleHelionNpc, - MaleLostNpc = Api.Enums.ApiCollectionType.MaleLostNpc, - FemaleLostNpc = Api.Enums.ApiCollectionType.FemaleLostNpc, - - MaleRavaNpc = Api.Enums.ApiCollectionType.MaleRavaNpc, - FemaleRavaNpc = Api.Enums.ApiCollectionType.FemaleRavaNpc, - MaleVeenaNpc = Api.Enums.ApiCollectionType.MaleVeenaNpc, - FemaleVeenaNpc = Api.Enums.ApiCollectionType.FemaleVeenaNpc, - - Default = Api.Enums.ApiCollectionType.Default, // The default collection was changed - Interface = Api.Enums.ApiCollectionType.Interface, // The ui collection was changed - Current = Api.Enums.ApiCollectionType.Current, // The current collection was changed - Individual, // An individual collection was changed - Inactive, // A collection was added or removed - Temporary, // A temporary collections was set or deleted via IPC -} - -public static class CollectionTypeExtensions -{ - public static bool IsSpecial( this CollectionType collectionType ) - => collectionType < CollectionType.Default; - - public static readonly (CollectionType, string, string)[] Special = Enum.GetValues< CollectionType >() - .Where( IsSpecial ) - .Select( s => ( s, s.ToName(), s.ToDescription() ) ) - .ToArray(); - - public static CollectionType FromParts( Gender gender, bool npc ) - { - gender = gender switch - { - Gender.MaleNpc => Gender.Male, - Gender.FemaleNpc => Gender.Female, - _ => gender, - }; - - return ( gender, npc ) switch - { - (Gender.Male, false) => CollectionType.MalePlayerCharacter, - (Gender.Female, false) => CollectionType.FemalePlayerCharacter, - (Gender.Male, true) => CollectionType.MaleNonPlayerCharacter, - (Gender.Female, true) => CollectionType.FemaleNonPlayerCharacter, - _ => CollectionType.Inactive, - }; - } - - // @formatter:off - private static readonly IReadOnlyList DefaultList = new[] { CollectionType.Default }; - private static readonly IReadOnlyList MalePlayerList = new[] { CollectionType.MalePlayerCharacter, CollectionType.Default }; - private static readonly IReadOnlyList FemalePlayerList = new[] { CollectionType.FemalePlayerCharacter, CollectionType.Default }; - private static readonly IReadOnlyList MaleNpcList = new[] { CollectionType.MaleNonPlayerCharacter, CollectionType.Default }; - private static readonly IReadOnlyList FemaleNpcList = new[] { CollectionType.FemaleNonPlayerCharacter, CollectionType.Default }; - // @formatter:on - - /// A list of definite redundancy possibilities. - public static IReadOnlyList InheritanceOrder(this CollectionType collectionType) - => collectionType switch - { - CollectionType.Yourself => DefaultList, - CollectionType.MalePlayerCharacter => DefaultList, - CollectionType.FemalePlayerCharacter => DefaultList, - CollectionType.MaleNonPlayerCharacter => DefaultList, - CollectionType.FemaleNonPlayerCharacter => DefaultList, - CollectionType.MaleMidlander => MalePlayerList, - CollectionType.FemaleMidlander => FemalePlayerList, - CollectionType.MaleHighlander => MalePlayerList, - CollectionType.FemaleHighlander => FemalePlayerList, - CollectionType.MaleWildwood => MalePlayerList, - CollectionType.FemaleWildwood => FemalePlayerList, - CollectionType.MaleDuskwight => MalePlayerList, - CollectionType.FemaleDuskwight => FemalePlayerList, - CollectionType.MalePlainsfolk => MalePlayerList, - CollectionType.FemalePlainsfolk => FemalePlayerList, - CollectionType.MaleDunesfolk => MalePlayerList, - CollectionType.FemaleDunesfolk => FemalePlayerList, - CollectionType.MaleSeekerOfTheSun => MalePlayerList, - CollectionType.FemaleSeekerOfTheSun => FemalePlayerList, - CollectionType.MaleKeeperOfTheMoon => MalePlayerList, - CollectionType.FemaleKeeperOfTheMoon => FemalePlayerList, - CollectionType.MaleSeawolf => MalePlayerList, - CollectionType.FemaleSeawolf => FemalePlayerList, - CollectionType.MaleHellsguard => MalePlayerList, - CollectionType.FemaleHellsguard => FemalePlayerList, - CollectionType.MaleRaen => MalePlayerList, - CollectionType.FemaleRaen => FemalePlayerList, - CollectionType.MaleXaela => MalePlayerList, - CollectionType.FemaleXaela => FemalePlayerList, - CollectionType.MaleHelion => MalePlayerList, - CollectionType.FemaleHelion => FemalePlayerList, - CollectionType.MaleLost => MalePlayerList, - CollectionType.FemaleLost => FemalePlayerList, - CollectionType.MaleRava => MalePlayerList, - CollectionType.FemaleRava => FemalePlayerList, - CollectionType.MaleVeena => MalePlayerList, - CollectionType.FemaleVeena => FemalePlayerList, - CollectionType.MaleMidlanderNpc => MaleNpcList, - CollectionType.FemaleMidlanderNpc => FemaleNpcList, - CollectionType.MaleHighlanderNpc => MaleNpcList, - CollectionType.FemaleHighlanderNpc => FemaleNpcList, - CollectionType.MaleWildwoodNpc => MaleNpcList, - CollectionType.FemaleWildwoodNpc => FemaleNpcList, - CollectionType.MaleDuskwightNpc => MaleNpcList, - CollectionType.FemaleDuskwightNpc => FemaleNpcList, - CollectionType.MalePlainsfolkNpc => MaleNpcList, - CollectionType.FemalePlainsfolkNpc => FemaleNpcList, - CollectionType.MaleDunesfolkNpc => MaleNpcList, - CollectionType.FemaleDunesfolkNpc => FemaleNpcList, - CollectionType.MaleSeekerOfTheSunNpc => MaleNpcList, - CollectionType.FemaleSeekerOfTheSunNpc => FemaleNpcList, - CollectionType.MaleKeeperOfTheMoonNpc => MaleNpcList, - CollectionType.FemaleKeeperOfTheMoonNpc => FemaleNpcList, - CollectionType.MaleSeawolfNpc => MaleNpcList, - CollectionType.FemaleSeawolfNpc => FemaleNpcList, - CollectionType.MaleHellsguardNpc => MaleNpcList, - CollectionType.FemaleHellsguardNpc => FemaleNpcList, - CollectionType.MaleRaenNpc => MaleNpcList, - CollectionType.FemaleRaenNpc => FemaleNpcList, - CollectionType.MaleXaelaNpc => MaleNpcList, - CollectionType.FemaleXaelaNpc => FemaleNpcList, - CollectionType.MaleHelionNpc => MaleNpcList, - CollectionType.FemaleHelionNpc => FemaleNpcList, - CollectionType.MaleLostNpc => MaleNpcList, - CollectionType.FemaleLostNpc => FemaleNpcList, - CollectionType.MaleRavaNpc => MaleNpcList, - CollectionType.FemaleRavaNpc => FemaleNpcList, - CollectionType.MaleVeenaNpc => MaleNpcList, - CollectionType.FemaleVeenaNpc => FemaleNpcList, - CollectionType.Individual => DefaultList, - _ => Array.Empty(), - }; - - public static CollectionType FromParts( SubRace race, Gender gender, bool npc ) - { - gender = gender switch - { - Gender.MaleNpc => Gender.Male, - Gender.FemaleNpc => Gender.Female, - _ => gender, - }; - - return ( race, gender, npc ) switch - { - (SubRace.Midlander, Gender.Male, false) => CollectionType.MaleMidlander, - (SubRace.Highlander, Gender.Male, false) => CollectionType.MaleHighlander, - (SubRace.Wildwood, Gender.Male, false) => CollectionType.MaleWildwood, - (SubRace.Duskwight, Gender.Male, false) => CollectionType.MaleDuskwight, - (SubRace.Plainsfolk, Gender.Male, false) => CollectionType.MalePlainsfolk, - (SubRace.Dunesfolk, Gender.Male, false) => CollectionType.MaleDunesfolk, - (SubRace.SeekerOfTheSun, Gender.Male, false) => CollectionType.MaleSeekerOfTheSun, - (SubRace.KeeperOfTheMoon, Gender.Male, false) => CollectionType.MaleKeeperOfTheMoon, - (SubRace.Seawolf, Gender.Male, false) => CollectionType.MaleSeawolf, - (SubRace.Hellsguard, Gender.Male, false) => CollectionType.MaleHellsguard, - (SubRace.Raen, Gender.Male, false) => CollectionType.MaleRaen, - (SubRace.Xaela, Gender.Male, false) => CollectionType.MaleXaela, - (SubRace.Helion, Gender.Male, false) => CollectionType.MaleHelion, - (SubRace.Lost, Gender.Male, false) => CollectionType.MaleLost, - (SubRace.Rava, Gender.Male, false) => CollectionType.MaleRava, - (SubRace.Veena, Gender.Male, false) => CollectionType.MaleVeena, - - (SubRace.Midlander, Gender.Female, false) => CollectionType.FemaleMidlander, - (SubRace.Highlander, Gender.Female, false) => CollectionType.FemaleHighlander, - (SubRace.Wildwood, Gender.Female, false) => CollectionType.FemaleWildwood, - (SubRace.Duskwight, Gender.Female, false) => CollectionType.FemaleDuskwight, - (SubRace.Plainsfolk, Gender.Female, false) => CollectionType.FemalePlainsfolk, - (SubRace.Dunesfolk, Gender.Female, false) => CollectionType.FemaleDunesfolk, - (SubRace.SeekerOfTheSun, Gender.Female, false) => CollectionType.FemaleSeekerOfTheSun, - (SubRace.KeeperOfTheMoon, Gender.Female, false) => CollectionType.FemaleKeeperOfTheMoon, - (SubRace.Seawolf, Gender.Female, false) => CollectionType.FemaleSeawolf, - (SubRace.Hellsguard, Gender.Female, false) => CollectionType.FemaleHellsguard, - (SubRace.Raen, Gender.Female, false) => CollectionType.FemaleRaen, - (SubRace.Xaela, Gender.Female, false) => CollectionType.FemaleXaela, - (SubRace.Helion, Gender.Female, false) => CollectionType.FemaleHelion, - (SubRace.Lost, Gender.Female, false) => CollectionType.FemaleLost, - (SubRace.Rava, Gender.Female, false) => CollectionType.FemaleRava, - (SubRace.Veena, Gender.Female, false) => CollectionType.FemaleVeena, - - (SubRace.Midlander, Gender.Male, true) => CollectionType.MaleMidlanderNpc, - (SubRace.Highlander, Gender.Male, true) => CollectionType.MaleHighlanderNpc, - (SubRace.Wildwood, Gender.Male, true) => CollectionType.MaleWildwoodNpc, - (SubRace.Duskwight, Gender.Male, true) => CollectionType.MaleDuskwightNpc, - (SubRace.Plainsfolk, Gender.Male, true) => CollectionType.MalePlainsfolkNpc, - (SubRace.Dunesfolk, Gender.Male, true) => CollectionType.MaleDunesfolkNpc, - (SubRace.SeekerOfTheSun, Gender.Male, true) => CollectionType.MaleSeekerOfTheSunNpc, - (SubRace.KeeperOfTheMoon, Gender.Male, true) => CollectionType.MaleKeeperOfTheMoonNpc, - (SubRace.Seawolf, Gender.Male, true) => CollectionType.MaleSeawolfNpc, - (SubRace.Hellsguard, Gender.Male, true) => CollectionType.MaleHellsguardNpc, - (SubRace.Raen, Gender.Male, true) => CollectionType.MaleRaenNpc, - (SubRace.Xaela, Gender.Male, true) => CollectionType.MaleXaelaNpc, - (SubRace.Helion, Gender.Male, true) => CollectionType.MaleHelionNpc, - (SubRace.Lost, Gender.Male, true) => CollectionType.MaleLostNpc, - (SubRace.Rava, Gender.Male, true) => CollectionType.MaleRavaNpc, - (SubRace.Veena, Gender.Male, true) => CollectionType.MaleVeenaNpc, - - (SubRace.Midlander, Gender.Female, true) => CollectionType.FemaleMidlanderNpc, - (SubRace.Highlander, Gender.Female, true) => CollectionType.FemaleHighlanderNpc, - (SubRace.Wildwood, Gender.Female, true) => CollectionType.FemaleWildwoodNpc, - (SubRace.Duskwight, Gender.Female, true) => CollectionType.FemaleDuskwightNpc, - (SubRace.Plainsfolk, Gender.Female, true) => CollectionType.FemalePlainsfolkNpc, - (SubRace.Dunesfolk, Gender.Female, true) => CollectionType.FemaleDunesfolkNpc, - (SubRace.SeekerOfTheSun, Gender.Female, true) => CollectionType.FemaleSeekerOfTheSunNpc, - (SubRace.KeeperOfTheMoon, Gender.Female, true) => CollectionType.FemaleKeeperOfTheMoonNpc, - (SubRace.Seawolf, Gender.Female, true) => CollectionType.FemaleSeawolfNpc, - (SubRace.Hellsguard, Gender.Female, true) => CollectionType.FemaleHellsguardNpc, - (SubRace.Raen, Gender.Female, true) => CollectionType.FemaleRaenNpc, - (SubRace.Xaela, Gender.Female, true) => CollectionType.FemaleXaelaNpc, - (SubRace.Helion, Gender.Female, true) => CollectionType.FemaleHelionNpc, - (SubRace.Lost, Gender.Female, true) => CollectionType.FemaleLostNpc, - (SubRace.Rava, Gender.Female, true) => CollectionType.FemaleRavaNpc, - (SubRace.Veena, Gender.Female, true) => CollectionType.FemaleVeenaNpc, - _ => CollectionType.Inactive, - }; - } - - public static bool TryParse( string text, out CollectionType type ) - { - if( Enum.TryParse( text, true, out type ) ) - { - return type is not CollectionType.Inactive and not CollectionType.Temporary; - } - - if( string.Equals( text, "character", StringComparison.OrdinalIgnoreCase ) ) - { - type = CollectionType.Individual; - return true; - } - - if( string.Equals( text, "base", StringComparison.OrdinalIgnoreCase ) ) - { - type = CollectionType.Default; - return true; - } - - if( string.Equals( text, "ui", StringComparison.OrdinalIgnoreCase ) ) - { - type = CollectionType.Interface; - return true; - } - - if( string.Equals( text, "selected", StringComparison.OrdinalIgnoreCase ) ) - { - type = CollectionType.Current; - return true; - } - - foreach( var t in Enum.GetValues< CollectionType >() ) - { - if( t is CollectionType.Inactive or CollectionType.Temporary ) - { - continue; - } - - if( string.Equals( text, t.ToName(), StringComparison.OrdinalIgnoreCase ) ) - { - type = t; - return true; - } - } - - return false; - } - - public static string ToName( this CollectionType collectionType ) - => collectionType switch - { - CollectionType.Yourself => "Your Character", - CollectionType.NonPlayerChild => "Non-Player Children", - CollectionType.NonPlayerElderly => "Non-Player Elderly", - CollectionType.MalePlayerCharacter => "Male Player Characters", - CollectionType.MaleNonPlayerCharacter => "Male Non-Player Characters", - CollectionType.MaleMidlander => $"Male {SubRace.Midlander.ToName()}", - CollectionType.MaleHighlander => $"Male {SubRace.Highlander.ToName()}", - CollectionType.MaleWildwood => $"Male {SubRace.Wildwood.ToName()}", - CollectionType.MaleDuskwight => $"Male {SubRace.Duskwight.ToName()}", - CollectionType.MalePlainsfolk => $"Male {SubRace.Plainsfolk.ToName()}", - CollectionType.MaleDunesfolk => $"Male {SubRace.Dunesfolk.ToName()}", - CollectionType.MaleSeekerOfTheSun => $"Male {SubRace.SeekerOfTheSun.ToName()}", - CollectionType.MaleKeeperOfTheMoon => $"Male {SubRace.KeeperOfTheMoon.ToName()}", - CollectionType.MaleSeawolf => $"Male {SubRace.Seawolf.ToName()}", - CollectionType.MaleHellsguard => $"Male {SubRace.Hellsguard.ToName()}", - CollectionType.MaleRaen => $"Male {SubRace.Raen.ToName()}", - CollectionType.MaleXaela => $"Male {SubRace.Xaela.ToName()}", - CollectionType.MaleHelion => $"Male {SubRace.Helion.ToName()}", - CollectionType.MaleLost => $"Male {SubRace.Lost.ToName()}", - CollectionType.MaleRava => $"Male {SubRace.Rava.ToName()}", - CollectionType.MaleVeena => $"Male {SubRace.Veena.ToName()}", - CollectionType.MaleMidlanderNpc => $"Male {SubRace.Midlander.ToName()} (NPC)", - CollectionType.MaleHighlanderNpc => $"Male {SubRace.Highlander.ToName()} (NPC)", - CollectionType.MaleWildwoodNpc => $"Male {SubRace.Wildwood.ToName()} (NPC)", - CollectionType.MaleDuskwightNpc => $"Male {SubRace.Duskwight.ToName()} (NPC)", - CollectionType.MalePlainsfolkNpc => $"Male {SubRace.Plainsfolk.ToName()} (NPC)", - CollectionType.MaleDunesfolkNpc => $"Male {SubRace.Dunesfolk.ToName()} (NPC)", - CollectionType.MaleSeekerOfTheSunNpc => $"Male {SubRace.SeekerOfTheSun.ToName()} (NPC)", - CollectionType.MaleKeeperOfTheMoonNpc => $"Male {SubRace.KeeperOfTheMoon.ToName()} (NPC)", - CollectionType.MaleSeawolfNpc => $"Male {SubRace.Seawolf.ToName()} (NPC)", - CollectionType.MaleHellsguardNpc => $"Male {SubRace.Hellsguard.ToName()} (NPC)", - CollectionType.MaleRaenNpc => $"Male {SubRace.Raen.ToName()} (NPC)", - CollectionType.MaleXaelaNpc => $"Male {SubRace.Xaela.ToName()} (NPC)", - CollectionType.MaleHelionNpc => $"Male {SubRace.Helion.ToName()} (NPC)", - CollectionType.MaleLostNpc => $"Male {SubRace.Lost.ToName()} (NPC)", - CollectionType.MaleRavaNpc => $"Male {SubRace.Rava.ToName()} (NPC)", - CollectionType.MaleVeenaNpc => $"Male {SubRace.Veena.ToName()} (NPC)", - CollectionType.FemalePlayerCharacter => "Female Player Characters", - CollectionType.FemaleNonPlayerCharacter => "Female Non-Player Characters", - CollectionType.FemaleMidlander => $"Female {SubRace.Midlander.ToName()}", - CollectionType.FemaleHighlander => $"Female {SubRace.Highlander.ToName()}", - CollectionType.FemaleWildwood => $"Female {SubRace.Wildwood.ToName()}", - CollectionType.FemaleDuskwight => $"Female {SubRace.Duskwight.ToName()}", - CollectionType.FemalePlainsfolk => $"Female {SubRace.Plainsfolk.ToName()}", - CollectionType.FemaleDunesfolk => $"Female {SubRace.Dunesfolk.ToName()}", - CollectionType.FemaleSeekerOfTheSun => $"Female {SubRace.SeekerOfTheSun.ToName()}", - CollectionType.FemaleKeeperOfTheMoon => $"Female {SubRace.KeeperOfTheMoon.ToName()}", - CollectionType.FemaleSeawolf => $"Female {SubRace.Seawolf.ToName()}", - CollectionType.FemaleHellsguard => $"Female {SubRace.Hellsguard.ToName()}", - CollectionType.FemaleRaen => $"Female {SubRace.Raen.ToName()}", - CollectionType.FemaleXaela => $"Female {SubRace.Xaela.ToName()}", - CollectionType.FemaleHelion => $"Female {SubRace.Helion.ToName()}", - CollectionType.FemaleLost => $"Female {SubRace.Lost.ToName()}", - CollectionType.FemaleRava => $"Female {SubRace.Rava.ToName()}", - CollectionType.FemaleVeena => $"Female {SubRace.Veena.ToName()}", - CollectionType.FemaleMidlanderNpc => $"Female {SubRace.Midlander.ToName()} (NPC)", - CollectionType.FemaleHighlanderNpc => $"Female {SubRace.Highlander.ToName()} (NPC)", - CollectionType.FemaleWildwoodNpc => $"Female {SubRace.Wildwood.ToName()} (NPC)", - CollectionType.FemaleDuskwightNpc => $"Female {SubRace.Duskwight.ToName()} (NPC)", - CollectionType.FemalePlainsfolkNpc => $"Female {SubRace.Plainsfolk.ToName()} (NPC)", - CollectionType.FemaleDunesfolkNpc => $"Female {SubRace.Dunesfolk.ToName()} (NPC)", - CollectionType.FemaleSeekerOfTheSunNpc => $"Female {SubRace.SeekerOfTheSun.ToName()} (NPC)", - CollectionType.FemaleKeeperOfTheMoonNpc => $"Female {SubRace.KeeperOfTheMoon.ToName()} (NPC)", - CollectionType.FemaleSeawolfNpc => $"Female {SubRace.Seawolf.ToName()} (NPC)", - CollectionType.FemaleHellsguardNpc => $"Female {SubRace.Hellsguard.ToName()} (NPC)", - CollectionType.FemaleRaenNpc => $"Female {SubRace.Raen.ToName()} (NPC)", - CollectionType.FemaleXaelaNpc => $"Female {SubRace.Xaela.ToName()} (NPC)", - CollectionType.FemaleHelionNpc => $"Female {SubRace.Helion.ToName()} (NPC)", - CollectionType.FemaleLostNpc => $"Female {SubRace.Lost.ToName()} (NPC)", - CollectionType.FemaleRavaNpc => $"Female {SubRace.Rava.ToName()} (NPC)", - CollectionType.FemaleVeenaNpc => $"Female {SubRace.Veena.ToName()} (NPC)", - CollectionType.Inactive => "Collection", - CollectionType.Default => "Default", - CollectionType.Interface => "Interface", - CollectionType.Individual => "Individual", - CollectionType.Current => "Current", - _ => string.Empty, - }; - - public static string ToDescription( this CollectionType collectionType ) - => collectionType switch - { - CollectionType.Yourself => "This collection applies to your own character, regardless of its name.\n" - + "It takes precedence before all other collections except for explicitly named individual collections.", - CollectionType.NonPlayerChild => - "This collection applies to all non-player characters with a child body-type.\n" - + "It takes precedence before all other collections except for explicitly named individual collections.", - CollectionType.NonPlayerElderly => - "This collection applies to all non-player characters with an elderly body-type.\n" - + "It takes precedence before all other collections except for explicitly named individual collections.", - CollectionType.MalePlayerCharacter => - "This collection applies to all male player characters that do not have a more specific character or racial collections associated.", - CollectionType.MaleNonPlayerCharacter => - "This collection applies to all human male non-player characters except those explicitly named. It takes precedence before the default and racial collections.", - CollectionType.MaleMidlander => - "This collection applies to all male player character Midlander Hyur that do not have a more specific character collection associated.", - CollectionType.MaleHighlander => - "This collection applies to all male player character Highlander Hyur that do not have a more specific character collection associated.", - CollectionType.MaleWildwood => - "This collection applies to all male player character Wildwood Elezen that do not have a more specific character collection associated.", - CollectionType.MaleDuskwight => - "This collection applies to all male player character Duskwight Elezen that do not have a more specific character collection associated.", - CollectionType.MalePlainsfolk => - "This collection applies to all male player character Plainsfolk Lalafell that do not have a more specific character collection associated.", - CollectionType.MaleDunesfolk => - "This collection applies to all male player character Dunesfolk Lalafell that do not have a more specific character collection associated.", - CollectionType.MaleSeekerOfTheSun => - "This collection applies to all male player character Seekers of the Sun that do not have a more specific character collection associated.", - CollectionType.MaleKeeperOfTheMoon => - "This collection applies to all male player character Keepers of the Moon that do not have a more specific character collection associated.", - CollectionType.MaleSeawolf => - "This collection applies to all male player character Sea Wolf Roegadyn that do not have a more specific character collection associated.", - CollectionType.MaleHellsguard => - "This collection applies to all male player character Hellsguard Roegadyn that do not have a more specific character collection associated.", - CollectionType.MaleRaen => - "This collection applies to all male player character Raen Au Ra that do not have a more specific character collection associated.", - CollectionType.MaleXaela => - "This collection applies to all male player character Xaela Au Ra that do not have a more specific character collection associated.", - CollectionType.MaleHelion => - "This collection applies to all male player character Helion Hrothgar that do not have a more specific character collection associated.", - CollectionType.MaleLost => - "This collection applies to all male player character Lost Hrothgar that do not have a more specific character collection associated.", - CollectionType.MaleRava => - "This collection applies to all male player character Rava Viera that do not have a more specific character collection associated.", - CollectionType.MaleVeena => - "This collection applies to all male player character Veena Viera that do not have a more specific character collection associated.", - CollectionType.MaleMidlanderNpc => - "This collection applies to all male non-player character Midlander Hyur that do not have a more specific character collection associated.", - CollectionType.MaleHighlanderNpc => - "This collection applies to all male non-player character Highlander Hyur that do not have a more specific character collection associated.", - CollectionType.MaleWildwoodNpc => - "This collection applies to all male non-player character Wildwood Elezen that do not have a more specific character collection associated.", - CollectionType.MaleDuskwightNpc => - "This collection applies to all male non-player character Duskwight Elezen that do not have a more specific character collection associated.", - CollectionType.MalePlainsfolkNpc => - "This collection applies to all male non-player character Plainsfolk Lalafell that do not have a more specific character collection associated.", - CollectionType.MaleDunesfolkNpc => - "This collection applies to all male non-player character Dunesfolk Lalafell that do not have a more specific character collection associated.", - CollectionType.MaleSeekerOfTheSunNpc => - "This collection applies to all male non-player character Seekers of the Sun that do not have a more specific character collection associated.", - CollectionType.MaleKeeperOfTheMoonNpc => - "This collection applies to all male non-player character Keepers of the Moon that do not have a more specific character collection associated.", - CollectionType.MaleSeawolfNpc => - "This collection applies to all male non-player character Sea Wolf Roegadyn that do not have a more specific character collection associated.", - CollectionType.MaleHellsguardNpc => - "This collection applies to all male non-player character Hellsguard Roegadyn that do not have a more specific character collection associated.", - CollectionType.MaleRaenNpc => - "This collection applies to all male non-player character Raen Au Ra that do not have a more specific character collection associated.", - CollectionType.MaleXaelaNpc => - "This collection applies to all male non-player character Xaela Au Ra that do not have a more specific character collection associated.", - CollectionType.MaleHelionNpc => - "This collection applies to all male non-player character Helion Hrothgar that do not have a more specific character collection associated.", - CollectionType.MaleLostNpc => - "This collection applies to all male non-player character Lost Hrothgar that do not have a more specific character collection associated.", - CollectionType.MaleRavaNpc => - "This collection applies to all male non-player character Rava Viera that do not have a more specific character collection associated.", - CollectionType.MaleVeenaNpc => - "This collection applies to all male non-player character Veena Viera that do not have a more specific character collection associated.", - CollectionType.FemalePlayerCharacter => - "This collection applies to all female player characters that do not have a more specific character or racial collections associated.", - CollectionType.FemaleNonPlayerCharacter => - "This collection applies to all human female non-player characters except those explicitly named. It takes precedence before the default and racial collections.", - CollectionType.FemaleMidlander => - "This collection applies to all female player character Midlander Hyur that do not have a more specific character collection associated.", - CollectionType.FemaleHighlander => - "This collection applies to all female player character Highlander Hyur that do not have a more specific character collection associated.", - CollectionType.FemaleWildwood => - "This collection applies to all female player character Wildwood Elezen that do not have a more specific character collection associated.", - CollectionType.FemaleDuskwight => - "This collection applies to all female player character Duskwight Elezen that do not have a more specific character collection associated.", - CollectionType.FemalePlainsfolk => - "This collection applies to all female player character Plainsfolk Lalafell that do not have a more specific character collection associated.", - CollectionType.FemaleDunesfolk => - "This collection applies to all female player character Dunesfolk Lalafell that do not have a more specific character collection associated.", - CollectionType.FemaleSeekerOfTheSun => - "This collection applies to all female player character Seekers of the Sun that do not have a more specific character collection associated.", - CollectionType.FemaleKeeperOfTheMoon => - "This collection applies to all female player character Keepers of the Moon that do not have a more specific character collection associated.", - CollectionType.FemaleSeawolf => - "This collection applies to all female player character Sea Wolf Roegadyn that do not have a more specific character collection associated.", - CollectionType.FemaleHellsguard => - "This collection applies to all female player character Hellsguard Roegadyn that do not have a more specific character collection associated.", - CollectionType.FemaleRaen => - "This collection applies to all female player character Raen Au Ra that do not have a more specific character collection associated.", - CollectionType.FemaleXaela => - "This collection applies to all female player character Xaela Au Ra that do not have a more specific character collection associated.", - CollectionType.FemaleHelion => - "This collection applies to all female player character Helion Hrothgar that do not have a more specific character collection associated.", - CollectionType.FemaleLost => - "This collection applies to all female player character Lost Hrothgar that do not have a more specific character collection associated.", - CollectionType.FemaleRava => - "This collection applies to all female player character Rava Viera that do not have a more specific character collection associated.", - CollectionType.FemaleVeena => - "This collection applies to all female player character Veena Viera that do not have a more specific character collection associated.", - CollectionType.FemaleMidlanderNpc => - "This collection applies to all female non-player character Midlander Hyur that do not have a more specific character collection associated.", - CollectionType.FemaleHighlanderNpc => - "This collection applies to all female non-player character Highlander Hyur that do not have a more specific character collection associated.", - CollectionType.FemaleWildwoodNpc => - "This collection applies to all female non-player character Wildwood Elezen that do not have a more specific character collection associated.", - CollectionType.FemaleDuskwightNpc => - "This collection applies to all female non-player character Duskwight Elezen that do not have a more specific character collection associated.", - CollectionType.FemalePlainsfolkNpc => - "This collection applies to all female non-player character Plainsfolk Lalafell that do not have a more specific character collection associated.", - CollectionType.FemaleDunesfolkNpc => - "This collection applies to all female non-player character Dunesfolk Lalafell that do not have a more specific character collection associated.", - CollectionType.FemaleSeekerOfTheSunNpc => - "This collection applies to all female non-player character Seekers of the Sun that do not have a more specific character collection associated.", - CollectionType.FemaleKeeperOfTheMoonNpc => - "This collection applies to all female non-player character Keepers of the Moon that do not have a more specific character collection associated.", - CollectionType.FemaleSeawolfNpc => - "This collection applies to all female non-player character Sea Wolf Roegadyn that do not have a more specific character collection associated.", - CollectionType.FemaleHellsguardNpc => - "This collection applies to all female non-player character Hellsguard Roegadyn that do not have a more specific character collection associated.", - CollectionType.FemaleRaenNpc => - "This collection applies to all female non-player character Raen Au Ra that do not have a more specific character collection associated.", - CollectionType.FemaleXaelaNpc => - "This collection applies to all female non-player character Xaela Au Ra that do not have a more specific character collection associated.", - CollectionType.FemaleHelionNpc => - "This collection applies to all female non-player character Helion Hrothgar that do not have a more specific character collection associated.", - CollectionType.FemaleLostNpc => - "This collection applies to all female non-player character Lost Hrothgar that do not have a more specific character collection associated.", - CollectionType.FemaleRavaNpc => - "This collection applies to all female non-player character Rava Viera that do not have a more specific character collection associated.", - CollectionType.FemaleVeenaNpc => - "This collection applies to all female non-player character Veena Viera that do not have a more specific character collection associated.", - _ => string.Empty, - }; -} \ No newline at end of file diff --git a/Penumbra/Collections/IndividualCollections.Files.cs b/Penumbra/Collections/IndividualCollections.Files.cs deleted file mode 100644 index 498688ed..00000000 --- a/Penumbra/Collections/IndividualCollections.Files.cs +++ /dev/null @@ -1,149 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Interface.Internal.Notifications; -using Newtonsoft.Json.Linq; -using Penumbra.GameData.Actors; -using Penumbra.String; -using Penumbra.Util; - -namespace Penumbra.Collections; - -public partial class IndividualCollections -{ - public JArray ToJObject() - { - var ret = new JArray(); - foreach( var (name, identifiers, collection) in Assignments ) - { - var tmp = identifiers[0].ToJson(); - tmp.Add( "Collection", collection.Name ); - tmp.Add( "Display", name ); - ret.Add( tmp ); - } - - return ret; - } - - public bool ReadJObject( JArray? obj, CollectionManager manager ) - { - if( obj == null ) - { - return true; - } - - var changes = false; - foreach( var data in obj ) - { - try - { - var identifier = Penumbra.Actors.FromJson( data as JObject ); - var group = GetGroup( identifier ); - if( group.Length == 0 || group.Any( i => !i.IsValid ) ) - { - changes = true; - Penumbra.ChatService.NotificationMessage( "Could not load an unknown individual collection, removed.", "Load Failure", NotificationType.Warning ); - continue; - } - - var collectionName = data[ "Collection" ]?.ToObject< string >() ?? string.Empty; - if( collectionName.Length == 0 || !manager.ByName( collectionName, out var collection ) ) - { - changes = true; - Penumbra.ChatService.NotificationMessage( $"Could not load the collection \"{collectionName}\" as individual collection for {identifier}, set to None.", "Load Failure", - NotificationType.Warning ); - continue; - } - - if( !Add( group, collection ) ) - { - changes = true; - Penumbra.ChatService.NotificationMessage( $"Could not add an individual collection for {identifier}, removed.", "Load Failure", - NotificationType.Warning ); - } - } - catch( Exception e ) - { - changes = true; - Penumbra.ChatService.NotificationMessage( $"Could not load an unknown individual collection, removed:\n{e}", "Load Failure", NotificationType.Error ); - } - } - - return changes; - } - - internal void Migrate0To1( Dictionary< string, ModCollection > old ) - { - static bool FindDataId( string name, IReadOnlyDictionary< uint, string > data, out uint dataId ) - { - var kvp = data.FirstOrDefault( kvp => kvp.Value.Equals( name, StringComparison.OrdinalIgnoreCase ), - new KeyValuePair< uint, string >( uint.MaxValue, string.Empty ) ); - dataId = kvp.Key; - return kvp.Value.Length > 0; - } - - foreach( var (name, collection) in old ) - { - var kind = ObjectKind.None; - var lowerName = name.ToLowerInvariant(); - // Prefer matching NPC names, fewer false positives than preferring players. - if( FindDataId( lowerName, _actorManager.Data.Companions, out var dataId ) ) - { - kind = ObjectKind.Companion; - } - else if( FindDataId( lowerName, _actorManager.Data.Mounts, out dataId ) ) - { - kind = ObjectKind.MountType; - } - else if( FindDataId( lowerName, _actorManager.Data.BNpcs, out dataId ) ) - { - kind = ObjectKind.BattleNpc; - } - else if( FindDataId( lowerName, _actorManager.Data.ENpcs, out dataId ) ) - { - kind = ObjectKind.EventNpc; - } - - var identifier = _actorManager.CreateNpc( kind, dataId ); - if( identifier.IsValid ) - { - // If the name corresponds to a valid npc, add it as a group. If this fails, notify users. - var group = GetGroup( identifier ); - var ids = string.Join( ", ", group.Select( i => i.DataId.ToString() ) ); - if( Add( $"{_actorManager.Data.ToName( kind, dataId )} ({kind.ToName()})", group, collection ) ) - { - Penumbra.Log.Information( $"Migrated {name} ({kind.ToName()}) to NPC Identifiers [{ids}]." ); - } - else - { - Penumbra.ChatService.NotificationMessage( - $"Could not migrate {name} ({collection.AnonymizedName}) which was assumed to be a {kind.ToName()} with IDs [{ids}], please look through your individual collections.", - "Migration Failure", NotificationType.Error ); - } - } - // If it is not a valid NPC name, check if it can be a player name. - else if( ActorManager.VerifyPlayerName( name ) ) - { - identifier = _actorManager.CreatePlayer( ByteString.FromStringUnsafe( name, false ), ushort.MaxValue ); - var shortName = string.Join( " ", name.Split().Select( n => $"{n[ 0 ]}." ) ); - // Try to migrate the player name without logging full names. - if( Add( $"{name} ({_actorManager.Data.ToWorldName( identifier.HomeWorld )})", new[] { identifier }, collection ) ) - { - Penumbra.Log.Information( $"Migrated {shortName} ({collection.AnonymizedName}) to Player Identifier." ); - } - else - { - Penumbra.ChatService.NotificationMessage( $"Could not migrate {shortName} ({collection.AnonymizedName}), please look through your individual collections.", - "Migration Failure", NotificationType.Error ); - } - } - else - { - Penumbra.ChatService.NotificationMessage( - $"Could not migrate {name} ({collection.AnonymizedName}), which can not be a player name nor is it a known NPC name, please look through your individual collections.", - "Migration Failure", NotificationType.Error ); - } - } - } -} \ No newline at end of file diff --git a/Penumbra/Collections/Manager/ActiveCollectionMigration.cs b/Penumbra/Collections/Manager/ActiveCollectionMigration.cs new file mode 100644 index 00000000..7126d0e2 --- /dev/null +++ b/Penumbra/Collections/Manager/ActiveCollectionMigration.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Dalamud.Interface.Internal.Notifications; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.Services; + +namespace Penumbra.Collections.Manager; + +public static class ActiveCollectionMigration +{ + /// Migrate ungendered collections to Male and Female for 0.5.9.0. + public static void MigrateUngenderedCollections(FilenameService fileNames) + { + if (!ActiveCollections.Load(fileNames, out var jObject)) + return; + + foreach (var (type, _, _) in CollectionTypeExtensions.Special.Where(t => t.Item2.StartsWith("Male "))) + { + var oldName = type.ToString()[4..]; + var value = jObject[oldName]; + if (value == null) + continue; + + jObject.Remove(oldName); + jObject.Add("Male" + oldName, value); + jObject.Add("Female" + oldName, value); + } + + using var stream = File.Open(fileNames.ActiveCollectionsFile, FileMode.Truncate); + using var writer = new StreamWriter(stream); + using var j = new JsonTextWriter(writer); + j.Formatting = Formatting.Indented; + jObject.WriteTo(j); + } + + /// Migrate individual collections to Identifiers for 0.6.0. + public static bool MigrateIndividualCollections(CollectionStorage storage, IndividualCollections individuals, JObject jObject) + { + var version = jObject[nameof(Version)]?.Value() ?? 0; + if (version > 0) + return false; + + // Load character collections. If a player name comes up multiple times, the last one is applied. + var characters = jObject["Characters"]?.ToObject>() ?? new Dictionary(); + var dict = new Dictionary(characters.Count); + foreach (var (player, collectionName) in characters) + { + if (!storage.ByName(collectionName, out var collection)) + { + Penumbra.ChatService.NotificationMessage( + $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {ModCollection.Empty.Name}.", "Load Failure", + NotificationType.Warning); + dict.Add(player, ModCollection.Empty); + } + else + { + dict.Add(player, collection); + } + } + + individuals.Migrate0To1(dict); + return true; + } +} diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs new file mode 100644 index 00000000..7b99b320 --- /dev/null +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -0,0 +1,469 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Dalamud.Interface.Internal.Notifications; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OtterGui; +using Penumbra.GameData.Actors; +using Penumbra.Services; +using Penumbra.UI; +using Penumbra.Util; +using static OtterGui.Raii.ImRaii; + +namespace Penumbra.Collections.Manager; + +public class ActiveCollections : ISavable, IDisposable +{ + public const int Version = 1; + + private readonly CollectionStorage _storage; + private readonly CommunicatorService _communicator; + private readonly SaveService _saveService; + + public ActiveCollections(CollectionStorage storage, ActorService actors, CommunicatorService communicator, SaveService saveService) + { + _storage = storage; + _communicator = communicator; + _saveService = saveService; + Current = storage.DefaultNamed; + Default = storage.DefaultNamed; + Interface = storage.DefaultNamed; + Individuals = new IndividualCollections(actors.AwaitedService); + _communicator.CollectionChange.Subscribe(OnCollectionChange); + LoadCollections(); + } + + public void Dispose() + => _communicator.CollectionChange.Unsubscribe(OnCollectionChange); + + /// The collection currently selected for changing settings. + public ModCollection Current { get; private set; } + + /// Whether the currently selected collection is used either directly via assignment or via inheritance. + public bool CurrentCollectionInUse { get; private set; } + + /// The collection used for general file redirections and all characters not specifically named. + public ModCollection Default { get; private set; } + + /// The collection used for all files categorized as UI files. + public ModCollection Interface { get; private set; } + + /// The list of individual assignments. + public readonly IndividualCollections Individuals; + + /// Get the collection assigned to an individual or Default if unassigned. + public ModCollection Individual(ActorIdentifier identifier) + => Individuals.TryGetCollection(identifier, out var c) ? c : Default; + + /// The list of group assignments. + private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues().Length - 3]; + + /// Return all actually assigned group assignments. + public IEnumerable> SpecialAssignments + { + get + { + for (var i = 0; i < _specialCollections.Length; ++i) + { + var collection = _specialCollections[i]; + if (collection != null) + yield return new KeyValuePair((CollectionType)i, collection); + } + } + } + + /// + public ModCollection? ByType(CollectionType type) + => ByType(type, ActorIdentifier.Invalid); + + /// Return the configured collection for the given type or null. + public ModCollection? ByType(CollectionType type, ActorIdentifier identifier) + { + if (type.IsSpecial()) + return _specialCollections[(int)type]; + + return type switch + { + CollectionType.Default => Default, + CollectionType.Interface => Interface, + CollectionType.Current => Current, + CollectionType.Individual => identifier.IsValid && Individuals.TryGetValue(identifier, out var c) ? c : null, + _ => null, + }; + } + + /// Create a special collection if it does not exist and set it to Empty. + public bool CreateSpecialCollection(CollectionType collectionType) + { + if (!collectionType.IsSpecial() || _specialCollections[(int)collectionType] != null) + return false; + + _specialCollections[(int)collectionType] = Default; + _communicator.CollectionChange.Invoke(collectionType, null, Default, string.Empty); + return true; + } + + /// Remove a special collection if it exists + public void RemoveSpecialCollection(CollectionType collectionType) + { + if (!collectionType.IsSpecial()) + return; + + var old = _specialCollections[(int)collectionType]; + if (old == null) + return; + + _specialCollections[(int)collectionType] = null; + _communicator.CollectionChange.Invoke(collectionType, old, null, string.Empty); + } + + /// Create an individual collection if possible. + public void CreateIndividualCollection(params ActorIdentifier[] identifiers) + { + if (Individuals.Add(identifiers, Default)) + _communicator.CollectionChange.Invoke(CollectionType.Individual, null, Default, Individuals.Last().DisplayName); + } + + /// Remove an individual collection if it exists. + public void RemoveIndividualCollection(int individualIndex) + { + if (individualIndex < 0 || individualIndex >= Individuals.Count) + return; + + var (name, old) = Individuals[individualIndex]; + if (Individuals.Delete(individualIndex)) + _communicator.CollectionChange.Invoke(CollectionType.Individual, old, null, name); + } + + /// Move an individual collection from one index to another. + public void MoveIndividualCollection(int from, int to) + { + if (Individuals.Move(from, to)) + _saveService.QueueSave(this); + } + + /// Set a active collection, can be used to set Default, Current, Interface, Special, or Individual collections. + public void SetCollection(ModCollection collection, CollectionType collectionType, int individualIndex = -1) + { + var oldCollection = collectionType switch + { + CollectionType.Default => Default, + CollectionType.Interface => Interface, + CollectionType.Current => Current, + CollectionType.Individual when individualIndex >= 0 && individualIndex < Individuals.Count => Individuals[individualIndex].Collection, + CollectionType.Individual => null, + _ when collectionType.IsSpecial() => _specialCollections[(int)collectionType] ?? Default, + _ => null, + }; + + if (oldCollection == null || collection == oldCollection || collection.Index >= _storage.Count) + return; + + switch (collectionType) + { + case CollectionType.Default: + Default = collection; + break; + case CollectionType.Interface: + Interface = collection; + break; + case CollectionType.Current: + Current = collection; + break; + case CollectionType.Individual: + if (!Individuals.ChangeCollection(individualIndex, collection)) + return; + + break; + default: + _specialCollections[(int)collectionType] = collection; + break; + } + + UpdateCurrentCollectionInUse(); + _communicator.CollectionChange.Invoke(collectionType, oldCollection, collection, + collectionType == CollectionType.Individual ? Individuals[individualIndex].DisplayName : string.Empty); + } + + public string ToFilename(FilenameService fileNames) + => fileNames.ActiveCollectionsFile; + + public string TypeName + => "Active Collections"; + + public string LogName(string _) + => "to file"; + + public void Save(StreamWriter writer) + { + var jObj = new JObject + { + { nameof(Version), Version }, + { nameof(Default), Default.Name }, + { nameof(Interface), Interface.Name }, + { nameof(Current), Current.Name }, + }; + foreach (var (type, collection) in _specialCollections.WithIndex().Where(p => p.Value != null) + .Select(p => ((CollectionType)p.Index, p.Value!))) + jObj.Add(type.ToString(), collection.Name); + + jObj.Add(nameof(Individuals), Individuals.ToJObject()); + using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + jObj.WriteTo(j); + } + + private void UpdateCurrentCollectionInUse() + => CurrentCollectionInUse = _specialCollections + .OfType() + .Prepend(Interface) + .Prepend(Default) + .Concat(Individuals.Assignments.Select(kvp => kvp.Collection)) + .SelectMany(c => c.GetFlattenedInheritance()).Contains(Current); + + /// Save if any of the active collections is changed and set new collections to Current. + private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string _3) + { + if (collectionType is CollectionType.Inactive) + { + if (newCollection != null) + { + SetCollection(newCollection, CollectionType.Current); + } + else if (oldCollection != null) + { + if (oldCollection == Default) + SetCollection(ModCollection.Empty, CollectionType.Default); + if (oldCollection == Interface) + SetCollection(ModCollection.Empty, CollectionType.Interface); + if (oldCollection == Current) + SetCollection(Default.Index > ModCollection.Empty.Index ? Default : _storage.DefaultNamed, CollectionType.Current); + + for (var i = 0; i < _specialCollections.Length; ++i) + { + if (oldCollection == _specialCollections[i]) + SetCollection(ModCollection.Empty, (CollectionType)i); + } + + for (var i = 0; i < Individuals.Count; ++i) + { + if (oldCollection == Individuals[i].Collection) + SetCollection(ModCollection.Empty, CollectionType.Individual, i); + } + } + } + else if (collectionType is not CollectionType.Temporary) + { + _saveService.QueueSave(this); + } + } + + /// + /// Load default, current, special, and character collections from config. + /// Then create caches. If a collection does not exist anymore, reset it to an appropriate default. + /// + private void LoadCollections() + { + var configChanged = !Load(_saveService.FileNames, out var jObject); + + // Load the default collection. If the string does not exist take the Default name if no file existed or the Empty name if one existed. + var defaultName = jObject[nameof(Default)]?.ToObject() + ?? (configChanged ? ModCollection.DefaultCollectionName : ModCollection.Empty.Name); + if (!_storage.ByName(defaultName, out var defaultCollection)) + { + Penumbra.ChatService.NotificationMessage( + $"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Name}.", + "Load Failure", + NotificationType.Warning); + Default = ModCollection.Empty; + configChanged = true; + } + else + { + Default = defaultCollection; + } + + // Load the interface collection. If no string is set, use the name of whatever was set as Default. + var interfaceName = jObject[nameof(Interface)]?.ToObject() ?? Default.Name; + if (!_storage.ByName(interfaceName, out var interfaceCollection)) + { + Penumbra.ChatService.NotificationMessage( + $"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Name}.", + "Load Failure", NotificationType.Warning); + Interface = ModCollection.Empty; + configChanged = true; + } + else + { + Interface = interfaceCollection; + } + + // Load the current collection. + var currentName = jObject[nameof(Current)]?.ToObject() ?? Default.Name; + if (!_storage.ByName(currentName, out var currentCollection)) + { + Penumbra.ChatService.NotificationMessage( + $"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollection.DefaultCollectionName}.", + "Load Failure", NotificationType.Warning); + Current = _storage.DefaultNamed; + configChanged = true; + } + else + { + Current = currentCollection; + } + + // Load special collections. + foreach (var (type, name, _) in CollectionTypeExtensions.Special) + { + var typeName = jObject[type.ToString()]?.ToObject(); + if (typeName != null) + { + if (!_storage.ByName(typeName, out var typeCollection)) + { + Penumbra.ChatService.NotificationMessage($"Last choice of {name} Collection {typeName} is not available, removed.", + "Load Failure", + NotificationType.Warning); + configChanged = true; + } + else + { + _specialCollections[(int)type] = typeCollection; + } + } + } + + configChanged |= ActiveCollectionMigration.MigrateIndividualCollections(_storage, Individuals, jObject); + configChanged |= Individuals.ReadJObject(jObject[nameof(Individuals)] as JArray, _storage); + + // Save any changes and create all required caches. + if (configChanged) + _saveService.ImmediateSave(this); + } + + /// + /// Read the active collection file into a jObject. + /// Returns true if this is successful, false if the file does not exist or it is unsuccessful. + /// + public static bool Load(FilenameService fileNames, out JObject ret) + { + var file = fileNames.ActiveCollectionsFile; + if (File.Exists(file)) + try + { + ret = JObject.Parse(File.ReadAllText(file)); + return true; + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not read active collections from file {file}:\n{e}"); + } + + ret = new JObject(); + return false; + } + + public string RedundancyCheck(CollectionType type, ActorIdentifier id) + { + var checkAssignment = ByType(type, id); + if (checkAssignment == null) + return string.Empty; + + switch (type) + { + // Check individual assignments. We can only be sure of redundancy for world-overlap or ownership overlap. + case CollectionType.Individual: + switch (id.Type) + { + case IdentifierType.Player when id.HomeWorld != ushort.MaxValue: + { + var global = ByType(CollectionType.Individual, Penumbra.Actors.CreatePlayer(id.PlayerName, ushort.MaxValue)); + return global?.Index == checkAssignment.Index + ? "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it." + : string.Empty; + } + case IdentifierType.Owned: + if (id.HomeWorld != ushort.MaxValue) + { + var global = ByType(CollectionType.Individual, + Penumbra.Actors.CreateOwned(id.PlayerName, ushort.MaxValue, id.Kind, id.DataId)); + if (global?.Index == checkAssignment.Index) + return "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it."; + } + + var unowned = ByType(CollectionType.Individual, Penumbra.Actors.CreateNpc(id.Kind, id.DataId)); + return unowned?.Index == checkAssignment.Index + ? "Assignment is redundant due to an identical unowned NPC assignment existing.\nYou can remove it." + : string.Empty; + } + + break; + // The group of all Characters is redundant if they are all equal to Default or unassigned. + case CollectionType.MalePlayerCharacter: + case CollectionType.MaleNonPlayerCharacter: + case CollectionType.FemalePlayerCharacter: + case CollectionType.FemaleNonPlayerCharacter: + var first = ByType(CollectionType.MalePlayerCharacter) ?? Default; + var second = ByType(CollectionType.MaleNonPlayerCharacter) ?? Default; + var third = ByType(CollectionType.FemalePlayerCharacter) ?? Default; + var fourth = ByType(CollectionType.FemaleNonPlayerCharacter) ?? Default; + if (first.Index == second.Index + && first.Index == third.Index + && first.Index == fourth.Index + && first.Index == Default.Index) + return + "Assignment is currently redundant due to the group [Male, Female, Player, NPC] Characters being unassigned or identical to each other and Default.\n" + + "You can keep just the Default Assignment."; + + break; + // Children and Elderly are redundant if they are identical to both Male NPCs and Female NPCs, or if they are unassigned to Default. + case CollectionType.NonPlayerChild: + case CollectionType.NonPlayerElderly: + var maleNpc = ByType(CollectionType.MaleNonPlayerCharacter); + var femaleNpc = ByType(CollectionType.FemaleNonPlayerCharacter); + var collection1 = CollectionType.MaleNonPlayerCharacter; + var collection2 = CollectionType.FemaleNonPlayerCharacter; + if (maleNpc == null) + { + maleNpc = Default; + if (maleNpc.Index != checkAssignment.Index) + return string.Empty; + + collection1 = CollectionType.Default; + } + + if (femaleNpc == null) + { + femaleNpc = Default; + if (femaleNpc.Index != checkAssignment.Index) + return string.Empty; + + collection2 = CollectionType.Default; + } + + return collection1 == collection2 + ? $"Assignment is currently redundant due to overwriting {collection1.ToName()} with an identical collection.\nYou can remove them." + : $"Assignment is currently redundant due to overwriting {collection1.ToName()} and {collection2.ToName()} with an identical collection.\nYou can remove them."; + + // For other assignments, check the inheritance order, unassigned means fall-through, + // assigned needs identical assignments to be redundant. + default: + var group = type.InheritanceOrder(); + foreach (var parentType in group) + { + var assignment = ByType(parentType); + if (assignment == null) + continue; + + if (assignment.Index == checkAssignment.Index) + return + $"Assignment is currently redundant due to overwriting {parentType.ToName()} with an identical collection.\nYou can remove it."; + } + + break; + } + + return string.Empty; + } +} diff --git a/Penumbra/Collections/Manager/CollectionCacheManager.cs b/Penumbra/Collections/Manager/CollectionCacheManager.cs new file mode 100644 index 00000000..a7ace6c1 --- /dev/null +++ b/Penumbra/Collections/Manager/CollectionCacheManager.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Penumbra.Api; +using Penumbra.Mods; +using Penumbra.Mods.Manager; +using Penumbra.Services; + +namespace Penumbra.Collections.Manager; + +public class CollectionCacheManager : IDisposable, IReadOnlyDictionary +{ + private readonly ActiveCollections _active; + private readonly CommunicatorService _communicator; + + private readonly Dictionary _cache = new(); + + public int Count + => _cache.Count; + + public IEnumerator> GetEnumerator() + => _cache.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public bool ContainsKey(ModCollection key) + => _cache.ContainsKey(key); + + public bool TryGetValue(ModCollection key, [NotNullWhen(true)] out ModCollectionCache? value) + => _cache.TryGetValue(key, out value); + + public ModCollectionCache this[ModCollection key] + => _cache[key]; + + public IEnumerable Keys + => _cache.Keys; + + public IEnumerable Values + => _cache.Values; + + public IEnumerable Active + => _cache.Keys.Where(c => c.Index > ModCollection.Empty.Index); + + public CollectionCacheManager(ActiveCollections active, CommunicatorService communicator) + { + _active = active; + _communicator = communicator; + + _communicator.CollectionChange.Subscribe(OnCollectionChange); + _communicator.ModPathChanged.Subscribe(OnModChangeAddition, -100); + _communicator.ModPathChanged.Subscribe(OnModChangeRemoval, 100); + _communicator.TemporaryGlobalModChange.Subscribe(OnGlobalModChange); + _communicator.ModOptionChanged.Subscribe(OnModOptionChange, -100); + CreateNecessaryCaches(); + } + + public void Dispose() + { + _communicator.CollectionChange.Unsubscribe(OnCollectionChange); + _communicator.ModPathChanged.Unsubscribe(OnModChangeAddition); + _communicator.ModPathChanged.Unsubscribe(OnModChangeRemoval); + _communicator.TemporaryGlobalModChange.Unsubscribe(OnGlobalModChange); + _communicator.ModOptionChanged.Unsubscribe(OnModOptionChange); + } + + /// + /// Cache handling. Usually recreate caches on the next framework tick, + /// but at launch create all of them at once. + /// + public void CreateNecessaryCaches() + { + var tasks = _active.SpecialAssignments.Select(p => p.Value) + .Concat(_active.Individuals.Select(p => p.Collection)) + .Prepend(_active.Current) + .Prepend(_active.Default) + .Prepend(_active.Interface) + .Distinct() + .Select(c => Task.Run(() => c.CalculateEffectiveFileListInternal(c == _active.Default))) + .ToArray(); + + Task.WaitAll(tasks); + } + + private void OnCollectionChange(CollectionType type, ModCollection? old, ModCollection? newCollection, string displayName) + { + if (type is CollectionType.Inactive) + return; + + var isDefault = type is CollectionType.Default; + if (newCollection?.Index > ModCollection.Empty.Index) + { + newCollection.CreateCache(isDefault); + _cache.TryAdd(newCollection, newCollection._cache!); + } + + RemoveCache(old); + } + + private void OnModChangeRemoval(ModPathChangeType type, Mod mod, DirectoryInfo? oldModPath, DirectoryInfo? newModPath) + { + switch (type) + { + case ModPathChangeType.Deleted: + case ModPathChangeType.StartingReload: + foreach (var collection in _cache.Keys.Where(c => c[mod.Index].Settings?.Enabled == true)) + collection._cache!.RemoveMod(mod, true); + break; + case ModPathChangeType.Moved: + foreach (var collection in _cache.Keys.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) + collection._cache!.ReloadMod(mod, true); + break; + } + } + + private void OnModChangeAddition(ModPathChangeType type, Mod mod, DirectoryInfo? oldModPath, DirectoryInfo? newModPath) + { + if (type is not (ModPathChangeType.Added or ModPathChangeType.Reloaded)) + return; + + foreach (var collection in _cache.Keys.Where(c => c[mod.Index].Settings?.Enabled == true)) + collection._cache!.AddMod(mod, true); + } + + /// Apply a mod change to all collections with a cache. + private void OnGlobalModChange(TemporaryMod mod, bool created, bool removed) + => TempModManager.OnGlobalModChange(_cache.Keys, mod, created, removed); + + /// Remove a cache from a collection if it is active. + private void RemoveCache(ModCollection? collection) + { + if (collection != null + && collection.Index > ModCollection.Empty.Index + && collection.Index != _active.Default.Index + && collection.Index != _active.Interface.Index + && collection.Index != _active.Current.Index + && _active.SpecialAssignments.All(c => c.Value.Index != collection.Index) + && _active.Individuals.All(c => c.Collection.Index != collection.Index)) + { + _cache.Remove(collection); + collection.ClearCache(); + } + } + + /// Prepare Changes by removing mods from caches with collections or add or reload mods. + private void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx) + { + if (type is ModOptionChangeType.PrepareChange) + { + foreach (var collection in _cache.Keys.Where(collection => collection[mod.Index].Settings is { Enabled: true })) + collection._cache!.RemoveMod(mod, false); + + return; + } + + type.HandlingInfo(out _, out var recomputeList, out var reload); + + if (!recomputeList) + return; + + foreach (var collection in _cache.Keys.Where(collection => collection[mod.Index].Settings is { Enabled: true })) + { + if (reload) + collection._cache!.ReloadMod(mod, true); + else + collection._cache!.AddMod(mod, true); + } + } +} diff --git a/Penumbra/Collections/Manager/CollectionManager.cs b/Penumbra/Collections/Manager/CollectionManager.cs new file mode 100644 index 00000000..b124b7db --- /dev/null +++ b/Penumbra/Collections/Manager/CollectionManager.cs @@ -0,0 +1,20 @@ +namespace Penumbra.Collections.Manager; + +public class CollectionManager +{ + public readonly CollectionStorage Storage; + public readonly ActiveCollections Active; + public readonly InheritanceManager Inheritances; + public readonly CollectionCacheManager Caches; + public readonly TempCollectionManager Temp; + + public CollectionManager(CollectionStorage storage, ActiveCollections active, InheritanceManager inheritances, + CollectionCacheManager caches, TempCollectionManager temp) + { + Storage = storage; + Active = active; + Inheritances = inheritances; + Caches = caches; + Temp = temp; + } +} diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs new file mode 100644 index 00000000..e363c106 --- /dev/null +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -0,0 +1,306 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using Dalamud.Interface.Internal.Notifications; +using OtterGui; +using OtterGui.Filesystem; +using Penumbra.Mods; +using Penumbra.Mods.Manager; +using Penumbra.Services; +using Penumbra.Util; + +namespace Penumbra.Collections.Manager; + +public class CollectionStorage : IReadOnlyList, IDisposable +{ + private readonly CommunicatorService _communicator; + private readonly SaveService _saveService; + + /// The empty collection is always available at Index 0. + private readonly List _collections = new() + { + ModCollection.Empty, + }; + + public readonly ModCollection DefaultNamed; + + /// Default enumeration skips the empty collection. + public IEnumerator GetEnumerator() + => _collections.Skip(1).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public IEnumerator GetEnumeratorWithEmpty() + => _collections.GetEnumerator(); + + public int Count + => _collections.Count; + + public ModCollection this[int index] + => _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.Name, name, StringComparison.OrdinalIgnoreCase), out collection); + + collection = ModCollection.Empty; + return true; + } + + public CollectionStorage(CommunicatorService communicator, SaveService saveService) + { + _communicator = communicator; + _saveService = saveService; + _communicator.ModDiscoveryStarted.Subscribe(OnModDiscoveryStarted); + _communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished); + _communicator.ModPathChanged.Subscribe(OnModPathChange, 10); + _communicator.ModOptionChanged.Subscribe(OnModOptionChange, 100); + ReadCollections(out DefaultNamed); + } + + public void Dispose() + { + _communicator.ModDiscoveryStarted.Unsubscribe(OnModDiscoveryStarted); + _communicator.ModDiscoveryFinished.Unsubscribe(OnModDiscoveryFinished); + _communicator.ModPathChanged.Unsubscribe(OnModPathChange); + _communicator.ModOptionChanged.Unsubscribe(OnModOptionChange); + } + + /// + /// Returns true if the name is not empty, it is not the name of the empty collection + /// and no existing collection results in the same filename as name. Also returns the fixed name. + /// + public bool CanAddCollection(string name, out string fixedName) + { + if (!IsValidName(name)) + { + fixedName = string.Empty; + return false; + } + + name = name.ToLowerInvariant(); + if (name.Length == 0 + || name == ModCollection.Empty.Name.ToLowerInvariant() + || _collections.Any(c => c.Name.ToLowerInvariant() == name)) + { + fixedName = string.Empty; + return false; + } + + fixedName = name; + return true; + } + + /// + /// Add a new collection of the given name. + /// If duplicate is not-null, the new collection will be a duplicate of it. + /// If the name of the collection would result in an already existing filename, skip it. + /// Returns true if the collection was successfully created and fires a Inactive event. + /// Also sets the current collection to the new collection afterwards. + /// + public bool AddCollection(string name, ModCollection? duplicate) + { + if (!CanAddCollection(name, out var fixedName)) + { + Penumbra.ChatService.NotificationMessage( + $"The new collection {name} would lead to the same path {fixedName} as one that already exists.", "Warning", + NotificationType.Warning); + return false; + } + + var newCollection = duplicate?.Duplicate(name) ?? ModCollection.CreateNewEmpty(name); + newCollection.Index = _collections.Count; + _collections.Add(newCollection); + + _saveService.ImmediateSave(newCollection); + Penumbra.ChatService.NotificationMessage($"Created new collection {newCollection.AnonymizedName}.", "Success", + NotificationType.Success); + _communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty); + return true; + } + + /// Whether the given collection can be deleted. + public bool CanRemoveCollection(ModCollection collection) + => collection.Index > ModCollection.Empty.Index && collection.Index < Count && collection.Index != DefaultNamed.Index; + + /// + /// Remove the given collection if it exists and is neither the empty nor the default-named collection. + /// + public bool RemoveCollection(ModCollection collection) + { + if (collection.Index <= ModCollection.Empty.Index || collection.Index >= _collections.Count) + { + Penumbra.ChatService.NotificationMessage("Can not remove the empty collection.", "Error", NotificationType.Error); + return false; + } + + if (collection.Index == DefaultNamed.Index) + { + Penumbra.ChatService.NotificationMessage("Can not remove the default collection.", "Error", NotificationType.Error); + return false; + } + + _saveService.ImmediateDelete(collection); + _collections.RemoveAt(collection.Index); + // Update indices. + for (var i = collection.Index; i < Count; ++i) + _collections[i].Index = i; + + Penumbra.ChatService.NotificationMessage($"Deleted collection {collection.AnonymizedName}.", "Success", NotificationType.Success); + _communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty); + return true; + } + + /// Stored after loading to be consumed and passed to the inheritance manager later. + private List>? _inheritancesByName = new(); + + /// Return an enumerable of collections and the collections they should inherit. + public IEnumerable<(ModCollection Collection, IReadOnlyList Inheritance, bool LoadChanges)> ConsumeInheritanceNames() + { + if (_inheritancesByName == null) + throw new Exception("Inheritances were already consumed. This method can not be called twice."); + + var inheritances = _inheritancesByName; + _inheritancesByName = null; + var list = new List(); + foreach (var (collection, inheritance) in _collections.Zip(inheritances)) + { + list.Clear(); + var changes = false; + foreach (var subCollectionName in inheritance) + { + if (ByName(subCollectionName, out var subCollection)) + { + list.Add(subCollection); + } + else + { + Penumbra.ChatService.NotificationMessage( + $"Inherited collection {subCollectionName} for {collection.AnonymizedName} does not exist, it was removed.", "Warning", + NotificationType.Warning); + changes = true; + } + } + + yield return (collection, list, changes); + } + } + + /// + /// Check if a name is valid to use for a collection. + /// Does not check for uniqueness. + /// + private static bool IsValidName(string name) + => name.Length > 0 && name.All(c => !c.IsInvalidAscii() && c is not '|' && !c.IsInvalidInPath()); + + /// + /// Read all collection files in the Collection Directory. + /// Ensure that the default named collection exists, and apply inheritances afterwards. + /// Duplicate collection files are not deleted, just not added here. + /// + private void ReadCollections(out ModCollection defaultNamedCollection) + { + _inheritancesByName?.Clear(); + _inheritancesByName?.Add(Array.Empty()); // None. + + foreach (var file in _saveService.FileNames.CollectionFiles) + { + var collection = ModCollection.LoadFromFile(file, out var inheritance); + if (collection == null || collection.Name.Length == 0) + continue; + + if (ByName(collection.Name, out _)) + { + Penumbra.ChatService.NotificationMessage($"Duplicate collection found: {collection.Name} already exists. Import skipped.", + "Warning", NotificationType.Warning); + continue; + } + + var correctName = _saveService.FileNames.CollectionFile(collection); + if (file.FullName != correctName) + Penumbra.ChatService.NotificationMessage($"Collection {file.Name} does not correspond to {collection.Name}.", "Warning", + NotificationType.Warning); + + _inheritancesByName?.Add(inheritance); + collection.Index = _collections.Count; + _collections.Add(collection); + } + + defaultNamedCollection = SetDefaultNamedCollection(); + } + + /// + /// Add the collection with the default name if it does not exist. + /// It should always be ensured that it exists, otherwise it will be created. + /// This can also not be deleted, so there are always at least the empty and a collection with default name. + /// + private ModCollection SetDefaultNamedCollection() + { + if (ByName(ModCollection.DefaultCollectionName, out var collection)) + return collection; + + if (AddCollection(ModCollection.DefaultCollectionName, null)) + return _collections[^1]; + + Penumbra.ChatService.NotificationMessage( + $"Unknown problem creating a collection with the name {ModCollection.DefaultCollectionName}, which is required to exist.", "Error", + NotificationType.Error); + return Count > 1 ? _collections[1] : _collections[0]; + } + + /// Move all settings in all collections to unused settings. + private void OnModDiscoveryStarted() + { + foreach (var collection in this) + collection.PrepareModDiscovery(); + } + + /// Restore all settings in all collections to mods. + private void OnModDiscoveryFinished() + { + // Re-apply all mod settings. + foreach (var collection in this) + collection.ApplyModSettings(); + } + + /// Add or remove a mod from all collections, or re-save all collections where the mod has settings. + private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, + DirectoryInfo? newDirectory) + { + switch (type) + { + case ModPathChangeType.Added: + foreach (var collection in this) + collection.AddMod(mod); + break; + case ModPathChangeType.Deleted: + foreach (var collection in this) + collection.RemoveMod(mod, mod.Index); + break; + case ModPathChangeType.Moved: + foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null)) + _saveService.QueueSave(collection); + break; + } + } + + /// Save all collections where the mod has settings and the change requires saving. + private void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx) + { + type.HandlingInfo(out var requiresSaving, out _, out _); + if (!requiresSaving) + return; + + foreach (var collection in this) + { + if (collection._settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false) + _saveService.QueueSave(collection); + } + } +} \ No newline at end of file diff --git a/Penumbra/Collections/Manager/CollectionType.cs b/Penumbra/Collections/Manager/CollectionType.cs new file mode 100644 index 00000000..52b48d9b --- /dev/null +++ b/Penumbra/Collections/Manager/CollectionType.cs @@ -0,0 +1,583 @@ +using Penumbra.GameData.Enums; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Penumbra.Collections.Manager; + +public enum CollectionType : byte +{ + // Special Collections + Yourself = Api.Enums.ApiCollectionType.Yourself, + + MalePlayerCharacter = Api.Enums.ApiCollectionType.MalePlayerCharacter, + FemalePlayerCharacter = Api.Enums.ApiCollectionType.FemalePlayerCharacter, + MaleNonPlayerCharacter = Api.Enums.ApiCollectionType.MaleNonPlayerCharacter, + FemaleNonPlayerCharacter = Api.Enums.ApiCollectionType.FemaleNonPlayerCharacter, + NonPlayerChild = Api.Enums.ApiCollectionType.NonPlayerChild, + NonPlayerElderly = Api.Enums.ApiCollectionType.NonPlayerElderly, + + MaleMidlander = Api.Enums.ApiCollectionType.MaleMidlander, + FemaleMidlander = Api.Enums.ApiCollectionType.FemaleMidlander, + MaleHighlander = Api.Enums.ApiCollectionType.MaleHighlander, + FemaleHighlander = Api.Enums.ApiCollectionType.FemaleHighlander, + + MaleWildwood = Api.Enums.ApiCollectionType.MaleWildwood, + FemaleWildwood = Api.Enums.ApiCollectionType.FemaleWildwood, + MaleDuskwight = Api.Enums.ApiCollectionType.MaleDuskwight, + FemaleDuskwight = Api.Enums.ApiCollectionType.FemaleDuskwight, + + MalePlainsfolk = Api.Enums.ApiCollectionType.MalePlainsfolk, + FemalePlainsfolk = Api.Enums.ApiCollectionType.FemalePlainsfolk, + MaleDunesfolk = Api.Enums.ApiCollectionType.MaleDunesfolk, + FemaleDunesfolk = Api.Enums.ApiCollectionType.FemaleDunesfolk, + + MaleSeekerOfTheSun = Api.Enums.ApiCollectionType.MaleSeekerOfTheSun, + FemaleSeekerOfTheSun = Api.Enums.ApiCollectionType.FemaleSeekerOfTheSun, + MaleKeeperOfTheMoon = Api.Enums.ApiCollectionType.MaleKeeperOfTheMoon, + FemaleKeeperOfTheMoon = Api.Enums.ApiCollectionType.FemaleKeeperOfTheMoon, + + MaleSeawolf = Api.Enums.ApiCollectionType.MaleSeawolf, + FemaleSeawolf = Api.Enums.ApiCollectionType.FemaleSeawolf, + MaleHellsguard = Api.Enums.ApiCollectionType.MaleHellsguard, + FemaleHellsguard = Api.Enums.ApiCollectionType.FemaleHellsguard, + + MaleRaen = Api.Enums.ApiCollectionType.MaleRaen, + FemaleRaen = Api.Enums.ApiCollectionType.FemaleRaen, + MaleXaela = Api.Enums.ApiCollectionType.MaleXaela, + FemaleXaela = Api.Enums.ApiCollectionType.FemaleXaela, + + MaleHelion = Api.Enums.ApiCollectionType.MaleHelion, + FemaleHelion = Api.Enums.ApiCollectionType.FemaleHelion, + MaleLost = Api.Enums.ApiCollectionType.MaleLost, + FemaleLost = Api.Enums.ApiCollectionType.FemaleLost, + + MaleRava = Api.Enums.ApiCollectionType.MaleRava, + FemaleRava = Api.Enums.ApiCollectionType.FemaleRava, + MaleVeena = Api.Enums.ApiCollectionType.MaleVeena, + FemaleVeena = Api.Enums.ApiCollectionType.FemaleVeena, + + MaleMidlanderNpc = Api.Enums.ApiCollectionType.MaleMidlanderNpc, + FemaleMidlanderNpc = Api.Enums.ApiCollectionType.FemaleMidlanderNpc, + MaleHighlanderNpc = Api.Enums.ApiCollectionType.MaleHighlanderNpc, + FemaleHighlanderNpc = Api.Enums.ApiCollectionType.FemaleHighlanderNpc, + + MaleWildwoodNpc = Api.Enums.ApiCollectionType.MaleWildwoodNpc, + FemaleWildwoodNpc = Api.Enums.ApiCollectionType.FemaleWildwoodNpc, + MaleDuskwightNpc = Api.Enums.ApiCollectionType.MaleDuskwightNpc, + FemaleDuskwightNpc = Api.Enums.ApiCollectionType.FemaleDuskwightNpc, + + MalePlainsfolkNpc = Api.Enums.ApiCollectionType.MalePlainsfolkNpc, + FemalePlainsfolkNpc = Api.Enums.ApiCollectionType.FemalePlainsfolkNpc, + MaleDunesfolkNpc = Api.Enums.ApiCollectionType.MaleDunesfolkNpc, + FemaleDunesfolkNpc = Api.Enums.ApiCollectionType.FemaleDunesfolkNpc, + + MaleSeekerOfTheSunNpc = Api.Enums.ApiCollectionType.MaleSeekerOfTheSunNpc, + FemaleSeekerOfTheSunNpc = Api.Enums.ApiCollectionType.FemaleSeekerOfTheSunNpc, + MaleKeeperOfTheMoonNpc = Api.Enums.ApiCollectionType.MaleKeeperOfTheMoonNpc, + FemaleKeeperOfTheMoonNpc = Api.Enums.ApiCollectionType.FemaleKeeperOfTheMoonNpc, + + MaleSeawolfNpc = Api.Enums.ApiCollectionType.MaleSeawolfNpc, + FemaleSeawolfNpc = Api.Enums.ApiCollectionType.FemaleSeawolfNpc, + MaleHellsguardNpc = Api.Enums.ApiCollectionType.MaleHellsguardNpc, + FemaleHellsguardNpc = Api.Enums.ApiCollectionType.FemaleHellsguardNpc, + + MaleRaenNpc = Api.Enums.ApiCollectionType.MaleRaenNpc, + FemaleRaenNpc = Api.Enums.ApiCollectionType.FemaleRaenNpc, + MaleXaelaNpc = Api.Enums.ApiCollectionType.MaleXaelaNpc, + FemaleXaelaNpc = Api.Enums.ApiCollectionType.FemaleXaelaNpc, + + MaleHelionNpc = Api.Enums.ApiCollectionType.MaleHelionNpc, + FemaleHelionNpc = Api.Enums.ApiCollectionType.FemaleHelionNpc, + MaleLostNpc = Api.Enums.ApiCollectionType.MaleLostNpc, + FemaleLostNpc = Api.Enums.ApiCollectionType.FemaleLostNpc, + + MaleRavaNpc = Api.Enums.ApiCollectionType.MaleRavaNpc, + FemaleRavaNpc = Api.Enums.ApiCollectionType.FemaleRavaNpc, + MaleVeenaNpc = Api.Enums.ApiCollectionType.MaleVeenaNpc, + FemaleVeenaNpc = Api.Enums.ApiCollectionType.FemaleVeenaNpc, + + Default = Api.Enums.ApiCollectionType.Default, // The default collection was changed + Interface = Api.Enums.ApiCollectionType.Interface, // The ui collection was changed + Current = Api.Enums.ApiCollectionType.Current, // The current collection was changed + Individual, // An individual collection was changed + Inactive, // A collection was added or removed + Temporary, // A temporary collections was set or deleted via IPC +} + +public static class CollectionTypeExtensions +{ + public static bool IsSpecial(this CollectionType collectionType) + => collectionType < CollectionType.Default; + + public static readonly (CollectionType, string, string)[] Special = Enum.GetValues() + .Where(IsSpecial) + .Select(s => (s, s.ToName(), s.ToDescription())) + .ToArray(); + + public static CollectionType FromParts(Gender gender, bool npc) + { + gender = gender switch + { + Gender.MaleNpc => Gender.Male, + Gender.FemaleNpc => Gender.Female, + _ => gender, + }; + + return (gender, npc) switch + { + (Gender.Male, false) => CollectionType.MalePlayerCharacter, + (Gender.Female, false) => CollectionType.FemalePlayerCharacter, + (Gender.Male, true) => CollectionType.MaleNonPlayerCharacter, + (Gender.Female, true) => CollectionType.FemaleNonPlayerCharacter, + _ => CollectionType.Inactive, + }; + } + + // @formatter:off + private static readonly IReadOnlyList DefaultList = new[] { CollectionType.Default }; + private static readonly IReadOnlyList MalePlayerList = new[] { CollectionType.MalePlayerCharacter, CollectionType.Default }; + private static readonly IReadOnlyList FemalePlayerList = new[] { CollectionType.FemalePlayerCharacter, CollectionType.Default }; + private static readonly IReadOnlyList MaleNpcList = new[] { CollectionType.MaleNonPlayerCharacter, CollectionType.Default }; + private static readonly IReadOnlyList FemaleNpcList = new[] { CollectionType.FemaleNonPlayerCharacter, CollectionType.Default }; + // @formatter:on + + /// A list of definite redundancy possibilities. + public static IReadOnlyList InheritanceOrder(this CollectionType collectionType) + => collectionType switch + { + CollectionType.Yourself => DefaultList, + CollectionType.MalePlayerCharacter => DefaultList, + CollectionType.FemalePlayerCharacter => DefaultList, + CollectionType.MaleNonPlayerCharacter => DefaultList, + CollectionType.FemaleNonPlayerCharacter => DefaultList, + CollectionType.MaleMidlander => MalePlayerList, + CollectionType.FemaleMidlander => FemalePlayerList, + CollectionType.MaleHighlander => MalePlayerList, + CollectionType.FemaleHighlander => FemalePlayerList, + CollectionType.MaleWildwood => MalePlayerList, + CollectionType.FemaleWildwood => FemalePlayerList, + CollectionType.MaleDuskwight => MalePlayerList, + CollectionType.FemaleDuskwight => FemalePlayerList, + CollectionType.MalePlainsfolk => MalePlayerList, + CollectionType.FemalePlainsfolk => FemalePlayerList, + CollectionType.MaleDunesfolk => MalePlayerList, + CollectionType.FemaleDunesfolk => FemalePlayerList, + CollectionType.MaleSeekerOfTheSun => MalePlayerList, + CollectionType.FemaleSeekerOfTheSun => FemalePlayerList, + CollectionType.MaleKeeperOfTheMoon => MalePlayerList, + CollectionType.FemaleKeeperOfTheMoon => FemalePlayerList, + CollectionType.MaleSeawolf => MalePlayerList, + CollectionType.FemaleSeawolf => FemalePlayerList, + CollectionType.MaleHellsguard => MalePlayerList, + CollectionType.FemaleHellsguard => FemalePlayerList, + CollectionType.MaleRaen => MalePlayerList, + CollectionType.FemaleRaen => FemalePlayerList, + CollectionType.MaleXaela => MalePlayerList, + CollectionType.FemaleXaela => FemalePlayerList, + CollectionType.MaleHelion => MalePlayerList, + CollectionType.FemaleHelion => FemalePlayerList, + CollectionType.MaleLost => MalePlayerList, + CollectionType.FemaleLost => FemalePlayerList, + CollectionType.MaleRava => MalePlayerList, + CollectionType.FemaleRava => FemalePlayerList, + CollectionType.MaleVeena => MalePlayerList, + CollectionType.FemaleVeena => FemalePlayerList, + CollectionType.MaleMidlanderNpc => MaleNpcList, + CollectionType.FemaleMidlanderNpc => FemaleNpcList, + CollectionType.MaleHighlanderNpc => MaleNpcList, + CollectionType.FemaleHighlanderNpc => FemaleNpcList, + CollectionType.MaleWildwoodNpc => MaleNpcList, + CollectionType.FemaleWildwoodNpc => FemaleNpcList, + CollectionType.MaleDuskwightNpc => MaleNpcList, + CollectionType.FemaleDuskwightNpc => FemaleNpcList, + CollectionType.MalePlainsfolkNpc => MaleNpcList, + CollectionType.FemalePlainsfolkNpc => FemaleNpcList, + CollectionType.MaleDunesfolkNpc => MaleNpcList, + CollectionType.FemaleDunesfolkNpc => FemaleNpcList, + CollectionType.MaleSeekerOfTheSunNpc => MaleNpcList, + CollectionType.FemaleSeekerOfTheSunNpc => FemaleNpcList, + CollectionType.MaleKeeperOfTheMoonNpc => MaleNpcList, + CollectionType.FemaleKeeperOfTheMoonNpc => FemaleNpcList, + CollectionType.MaleSeawolfNpc => MaleNpcList, + CollectionType.FemaleSeawolfNpc => FemaleNpcList, + CollectionType.MaleHellsguardNpc => MaleNpcList, + CollectionType.FemaleHellsguardNpc => FemaleNpcList, + CollectionType.MaleRaenNpc => MaleNpcList, + CollectionType.FemaleRaenNpc => FemaleNpcList, + CollectionType.MaleXaelaNpc => MaleNpcList, + CollectionType.FemaleXaelaNpc => FemaleNpcList, + CollectionType.MaleHelionNpc => MaleNpcList, + CollectionType.FemaleHelionNpc => FemaleNpcList, + CollectionType.MaleLostNpc => MaleNpcList, + CollectionType.FemaleLostNpc => FemaleNpcList, + CollectionType.MaleRavaNpc => MaleNpcList, + CollectionType.FemaleRavaNpc => FemaleNpcList, + CollectionType.MaleVeenaNpc => MaleNpcList, + CollectionType.FemaleVeenaNpc => FemaleNpcList, + CollectionType.Individual => DefaultList, + _ => Array.Empty(), + }; + + public static CollectionType FromParts(SubRace race, Gender gender, bool npc) + { + gender = gender switch + { + Gender.MaleNpc => Gender.Male, + Gender.FemaleNpc => Gender.Female, + _ => gender, + }; + + return (race, gender, npc) switch + { + (SubRace.Midlander, Gender.Male, false) => CollectionType.MaleMidlander, + (SubRace.Highlander, Gender.Male, false) => CollectionType.MaleHighlander, + (SubRace.Wildwood, Gender.Male, false) => CollectionType.MaleWildwood, + (SubRace.Duskwight, Gender.Male, false) => CollectionType.MaleDuskwight, + (SubRace.Plainsfolk, Gender.Male, false) => CollectionType.MalePlainsfolk, + (SubRace.Dunesfolk, Gender.Male, false) => CollectionType.MaleDunesfolk, + (SubRace.SeekerOfTheSun, Gender.Male, false) => CollectionType.MaleSeekerOfTheSun, + (SubRace.KeeperOfTheMoon, Gender.Male, false) => CollectionType.MaleKeeperOfTheMoon, + (SubRace.Seawolf, Gender.Male, false) => CollectionType.MaleSeawolf, + (SubRace.Hellsguard, Gender.Male, false) => CollectionType.MaleHellsguard, + (SubRace.Raen, Gender.Male, false) => CollectionType.MaleRaen, + (SubRace.Xaela, Gender.Male, false) => CollectionType.MaleXaela, + (SubRace.Helion, Gender.Male, false) => CollectionType.MaleHelion, + (SubRace.Lost, Gender.Male, false) => CollectionType.MaleLost, + (SubRace.Rava, Gender.Male, false) => CollectionType.MaleRava, + (SubRace.Veena, Gender.Male, false) => CollectionType.MaleVeena, + + (SubRace.Midlander, Gender.Female, false) => CollectionType.FemaleMidlander, + (SubRace.Highlander, Gender.Female, false) => CollectionType.FemaleHighlander, + (SubRace.Wildwood, Gender.Female, false) => CollectionType.FemaleWildwood, + (SubRace.Duskwight, Gender.Female, false) => CollectionType.FemaleDuskwight, + (SubRace.Plainsfolk, Gender.Female, false) => CollectionType.FemalePlainsfolk, + (SubRace.Dunesfolk, Gender.Female, false) => CollectionType.FemaleDunesfolk, + (SubRace.SeekerOfTheSun, Gender.Female, false) => CollectionType.FemaleSeekerOfTheSun, + (SubRace.KeeperOfTheMoon, Gender.Female, false) => CollectionType.FemaleKeeperOfTheMoon, + (SubRace.Seawolf, Gender.Female, false) => CollectionType.FemaleSeawolf, + (SubRace.Hellsguard, Gender.Female, false) => CollectionType.FemaleHellsguard, + (SubRace.Raen, Gender.Female, false) => CollectionType.FemaleRaen, + (SubRace.Xaela, Gender.Female, false) => CollectionType.FemaleXaela, + (SubRace.Helion, Gender.Female, false) => CollectionType.FemaleHelion, + (SubRace.Lost, Gender.Female, false) => CollectionType.FemaleLost, + (SubRace.Rava, Gender.Female, false) => CollectionType.FemaleRava, + (SubRace.Veena, Gender.Female, false) => CollectionType.FemaleVeena, + + (SubRace.Midlander, Gender.Male, true) => CollectionType.MaleMidlanderNpc, + (SubRace.Highlander, Gender.Male, true) => CollectionType.MaleHighlanderNpc, + (SubRace.Wildwood, Gender.Male, true) => CollectionType.MaleWildwoodNpc, + (SubRace.Duskwight, Gender.Male, true) => CollectionType.MaleDuskwightNpc, + (SubRace.Plainsfolk, Gender.Male, true) => CollectionType.MalePlainsfolkNpc, + (SubRace.Dunesfolk, Gender.Male, true) => CollectionType.MaleDunesfolkNpc, + (SubRace.SeekerOfTheSun, Gender.Male, true) => CollectionType.MaleSeekerOfTheSunNpc, + (SubRace.KeeperOfTheMoon, Gender.Male, true) => CollectionType.MaleKeeperOfTheMoonNpc, + (SubRace.Seawolf, Gender.Male, true) => CollectionType.MaleSeawolfNpc, + (SubRace.Hellsguard, Gender.Male, true) => CollectionType.MaleHellsguardNpc, + (SubRace.Raen, Gender.Male, true) => CollectionType.MaleRaenNpc, + (SubRace.Xaela, Gender.Male, true) => CollectionType.MaleXaelaNpc, + (SubRace.Helion, Gender.Male, true) => CollectionType.MaleHelionNpc, + (SubRace.Lost, Gender.Male, true) => CollectionType.MaleLostNpc, + (SubRace.Rava, Gender.Male, true) => CollectionType.MaleRavaNpc, + (SubRace.Veena, Gender.Male, true) => CollectionType.MaleVeenaNpc, + + (SubRace.Midlander, Gender.Female, true) => CollectionType.FemaleMidlanderNpc, + (SubRace.Highlander, Gender.Female, true) => CollectionType.FemaleHighlanderNpc, + (SubRace.Wildwood, Gender.Female, true) => CollectionType.FemaleWildwoodNpc, + (SubRace.Duskwight, Gender.Female, true) => CollectionType.FemaleDuskwightNpc, + (SubRace.Plainsfolk, Gender.Female, true) => CollectionType.FemalePlainsfolkNpc, + (SubRace.Dunesfolk, Gender.Female, true) => CollectionType.FemaleDunesfolkNpc, + (SubRace.SeekerOfTheSun, Gender.Female, true) => CollectionType.FemaleSeekerOfTheSunNpc, + (SubRace.KeeperOfTheMoon, Gender.Female, true) => CollectionType.FemaleKeeperOfTheMoonNpc, + (SubRace.Seawolf, Gender.Female, true) => CollectionType.FemaleSeawolfNpc, + (SubRace.Hellsguard, Gender.Female, true) => CollectionType.FemaleHellsguardNpc, + (SubRace.Raen, Gender.Female, true) => CollectionType.FemaleRaenNpc, + (SubRace.Xaela, Gender.Female, true) => CollectionType.FemaleXaelaNpc, + (SubRace.Helion, Gender.Female, true) => CollectionType.FemaleHelionNpc, + (SubRace.Lost, Gender.Female, true) => CollectionType.FemaleLostNpc, + (SubRace.Rava, Gender.Female, true) => CollectionType.FemaleRavaNpc, + (SubRace.Veena, Gender.Female, true) => CollectionType.FemaleVeenaNpc, + _ => CollectionType.Inactive, + }; + } + + public static bool TryParse(string text, out CollectionType type) + { + if (Enum.TryParse(text, true, out type)) + { + return type is not CollectionType.Inactive and not CollectionType.Temporary; + } + + if (string.Equals(text, "character", StringComparison.OrdinalIgnoreCase)) + { + type = CollectionType.Individual; + return true; + } + + if (string.Equals(text, "base", StringComparison.OrdinalIgnoreCase)) + { + type = CollectionType.Default; + return true; + } + + if (string.Equals(text, "ui", StringComparison.OrdinalIgnoreCase)) + { + type = CollectionType.Interface; + return true; + } + + if (string.Equals(text, "selected", StringComparison.OrdinalIgnoreCase)) + { + type = CollectionType.Current; + return true; + } + + foreach (var t in Enum.GetValues()) + { + if (t is CollectionType.Inactive or CollectionType.Temporary) + { + continue; + } + + if (string.Equals(text, t.ToName(), StringComparison.OrdinalIgnoreCase)) + { + type = t; + return true; + } + } + + return false; + } + + public static string ToName(this CollectionType collectionType) + => collectionType switch + { + CollectionType.Yourself => "Your Character", + CollectionType.NonPlayerChild => "Non-Player Children", + CollectionType.NonPlayerElderly => "Non-Player Elderly", + CollectionType.MalePlayerCharacter => "Male Player Characters", + CollectionType.MaleNonPlayerCharacter => "Male Non-Player Characters", + CollectionType.MaleMidlander => $"Male {SubRace.Midlander.ToName()}", + CollectionType.MaleHighlander => $"Male {SubRace.Highlander.ToName()}", + CollectionType.MaleWildwood => $"Male {SubRace.Wildwood.ToName()}", + CollectionType.MaleDuskwight => $"Male {SubRace.Duskwight.ToName()}", + CollectionType.MalePlainsfolk => $"Male {SubRace.Plainsfolk.ToName()}", + CollectionType.MaleDunesfolk => $"Male {SubRace.Dunesfolk.ToName()}", + CollectionType.MaleSeekerOfTheSun => $"Male {SubRace.SeekerOfTheSun.ToName()}", + CollectionType.MaleKeeperOfTheMoon => $"Male {SubRace.KeeperOfTheMoon.ToName()}", + CollectionType.MaleSeawolf => $"Male {SubRace.Seawolf.ToName()}", + CollectionType.MaleHellsguard => $"Male {SubRace.Hellsguard.ToName()}", + CollectionType.MaleRaen => $"Male {SubRace.Raen.ToName()}", + CollectionType.MaleXaela => $"Male {SubRace.Xaela.ToName()}", + CollectionType.MaleHelion => $"Male {SubRace.Helion.ToName()}", + CollectionType.MaleLost => $"Male {SubRace.Lost.ToName()}", + CollectionType.MaleRava => $"Male {SubRace.Rava.ToName()}", + CollectionType.MaleVeena => $"Male {SubRace.Veena.ToName()}", + CollectionType.MaleMidlanderNpc => $"Male {SubRace.Midlander.ToName()} (NPC)", + CollectionType.MaleHighlanderNpc => $"Male {SubRace.Highlander.ToName()} (NPC)", + CollectionType.MaleWildwoodNpc => $"Male {SubRace.Wildwood.ToName()} (NPC)", + CollectionType.MaleDuskwightNpc => $"Male {SubRace.Duskwight.ToName()} (NPC)", + CollectionType.MalePlainsfolkNpc => $"Male {SubRace.Plainsfolk.ToName()} (NPC)", + CollectionType.MaleDunesfolkNpc => $"Male {SubRace.Dunesfolk.ToName()} (NPC)", + CollectionType.MaleSeekerOfTheSunNpc => $"Male {SubRace.SeekerOfTheSun.ToName()} (NPC)", + CollectionType.MaleKeeperOfTheMoonNpc => $"Male {SubRace.KeeperOfTheMoon.ToName()} (NPC)", + CollectionType.MaleSeawolfNpc => $"Male {SubRace.Seawolf.ToName()} (NPC)", + CollectionType.MaleHellsguardNpc => $"Male {SubRace.Hellsguard.ToName()} (NPC)", + CollectionType.MaleRaenNpc => $"Male {SubRace.Raen.ToName()} (NPC)", + CollectionType.MaleXaelaNpc => $"Male {SubRace.Xaela.ToName()} (NPC)", + CollectionType.MaleHelionNpc => $"Male {SubRace.Helion.ToName()} (NPC)", + CollectionType.MaleLostNpc => $"Male {SubRace.Lost.ToName()} (NPC)", + CollectionType.MaleRavaNpc => $"Male {SubRace.Rava.ToName()} (NPC)", + CollectionType.MaleVeenaNpc => $"Male {SubRace.Veena.ToName()} (NPC)", + CollectionType.FemalePlayerCharacter => "Female Player Characters", + CollectionType.FemaleNonPlayerCharacter => "Female Non-Player Characters", + CollectionType.FemaleMidlander => $"Female {SubRace.Midlander.ToName()}", + CollectionType.FemaleHighlander => $"Female {SubRace.Highlander.ToName()}", + CollectionType.FemaleWildwood => $"Female {SubRace.Wildwood.ToName()}", + CollectionType.FemaleDuskwight => $"Female {SubRace.Duskwight.ToName()}", + CollectionType.FemalePlainsfolk => $"Female {SubRace.Plainsfolk.ToName()}", + CollectionType.FemaleDunesfolk => $"Female {SubRace.Dunesfolk.ToName()}", + CollectionType.FemaleSeekerOfTheSun => $"Female {SubRace.SeekerOfTheSun.ToName()}", + CollectionType.FemaleKeeperOfTheMoon => $"Female {SubRace.KeeperOfTheMoon.ToName()}", + CollectionType.FemaleSeawolf => $"Female {SubRace.Seawolf.ToName()}", + CollectionType.FemaleHellsguard => $"Female {SubRace.Hellsguard.ToName()}", + CollectionType.FemaleRaen => $"Female {SubRace.Raen.ToName()}", + CollectionType.FemaleXaela => $"Female {SubRace.Xaela.ToName()}", + CollectionType.FemaleHelion => $"Female {SubRace.Helion.ToName()}", + CollectionType.FemaleLost => $"Female {SubRace.Lost.ToName()}", + CollectionType.FemaleRava => $"Female {SubRace.Rava.ToName()}", + CollectionType.FemaleVeena => $"Female {SubRace.Veena.ToName()}", + CollectionType.FemaleMidlanderNpc => $"Female {SubRace.Midlander.ToName()} (NPC)", + CollectionType.FemaleHighlanderNpc => $"Female {SubRace.Highlander.ToName()} (NPC)", + CollectionType.FemaleWildwoodNpc => $"Female {SubRace.Wildwood.ToName()} (NPC)", + CollectionType.FemaleDuskwightNpc => $"Female {SubRace.Duskwight.ToName()} (NPC)", + CollectionType.FemalePlainsfolkNpc => $"Female {SubRace.Plainsfolk.ToName()} (NPC)", + CollectionType.FemaleDunesfolkNpc => $"Female {SubRace.Dunesfolk.ToName()} (NPC)", + CollectionType.FemaleSeekerOfTheSunNpc => $"Female {SubRace.SeekerOfTheSun.ToName()} (NPC)", + CollectionType.FemaleKeeperOfTheMoonNpc => $"Female {SubRace.KeeperOfTheMoon.ToName()} (NPC)", + CollectionType.FemaleSeawolfNpc => $"Female {SubRace.Seawolf.ToName()} (NPC)", + CollectionType.FemaleHellsguardNpc => $"Female {SubRace.Hellsguard.ToName()} (NPC)", + CollectionType.FemaleRaenNpc => $"Female {SubRace.Raen.ToName()} (NPC)", + CollectionType.FemaleXaelaNpc => $"Female {SubRace.Xaela.ToName()} (NPC)", + CollectionType.FemaleHelionNpc => $"Female {SubRace.Helion.ToName()} (NPC)", + CollectionType.FemaleLostNpc => $"Female {SubRace.Lost.ToName()} (NPC)", + CollectionType.FemaleRavaNpc => $"Female {SubRace.Rava.ToName()} (NPC)", + CollectionType.FemaleVeenaNpc => $"Female {SubRace.Veena.ToName()} (NPC)", + CollectionType.Inactive => "Collection", + CollectionType.Default => "Default", + CollectionType.Interface => "Interface", + CollectionType.Individual => "Individual", + CollectionType.Current => "Current", + _ => string.Empty, + }; + + public static string ToDescription(this CollectionType collectionType) + => collectionType switch + { + CollectionType.Yourself => "This collection applies to your own character, regardless of its name.\n" + + "It takes precedence before all other collections except for explicitly named individual collections.", + CollectionType.NonPlayerChild => + "This collection applies to all non-player characters with a child body-type.\n" + + "It takes precedence before all other collections except for explicitly named individual collections.", + CollectionType.NonPlayerElderly => + "This collection applies to all non-player characters with an elderly body-type.\n" + + "It takes precedence before all other collections except for explicitly named individual collections.", + CollectionType.MalePlayerCharacter => + "This collection applies to all male player characters that do not have a more specific character or racial collections associated.", + CollectionType.MaleNonPlayerCharacter => + "This collection applies to all human male non-player characters except those explicitly named. It takes precedence before the default and racial collections.", + CollectionType.MaleMidlander => + "This collection applies to all male player character Midlander Hyur that do not have a more specific character collection associated.", + CollectionType.MaleHighlander => + "This collection applies to all male player character Highlander Hyur that do not have a more specific character collection associated.", + CollectionType.MaleWildwood => + "This collection applies to all male player character Wildwood Elezen that do not have a more specific character collection associated.", + CollectionType.MaleDuskwight => + "This collection applies to all male player character Duskwight Elezen that do not have a more specific character collection associated.", + CollectionType.MalePlainsfolk => + "This collection applies to all male player character Plainsfolk Lalafell that do not have a more specific character collection associated.", + CollectionType.MaleDunesfolk => + "This collection applies to all male player character Dunesfolk Lalafell that do not have a more specific character collection associated.", + CollectionType.MaleSeekerOfTheSun => + "This collection applies to all male player character Seekers of the Sun that do not have a more specific character collection associated.", + CollectionType.MaleKeeperOfTheMoon => + "This collection applies to all male player character Keepers of the Moon that do not have a more specific character collection associated.", + CollectionType.MaleSeawolf => + "This collection applies to all male player character Sea Wolf Roegadyn that do not have a more specific character collection associated.", + CollectionType.MaleHellsguard => + "This collection applies to all male player character Hellsguard Roegadyn that do not have a more specific character collection associated.", + CollectionType.MaleRaen => + "This collection applies to all male player character Raen Au Ra that do not have a more specific character collection associated.", + CollectionType.MaleXaela => + "This collection applies to all male player character Xaela Au Ra that do not have a more specific character collection associated.", + CollectionType.MaleHelion => + "This collection applies to all male player character Helion Hrothgar that do not have a more specific character collection associated.", + CollectionType.MaleLost => + "This collection applies to all male player character Lost Hrothgar that do not have a more specific character collection associated.", + CollectionType.MaleRava => + "This collection applies to all male player character Rava Viera that do not have a more specific character collection associated.", + CollectionType.MaleVeena => + "This collection applies to all male player character Veena Viera that do not have a more specific character collection associated.", + CollectionType.MaleMidlanderNpc => + "This collection applies to all male non-player character Midlander Hyur that do not have a more specific character collection associated.", + CollectionType.MaleHighlanderNpc => + "This collection applies to all male non-player character Highlander Hyur that do not have a more specific character collection associated.", + CollectionType.MaleWildwoodNpc => + "This collection applies to all male non-player character Wildwood Elezen that do not have a more specific character collection associated.", + CollectionType.MaleDuskwightNpc => + "This collection applies to all male non-player character Duskwight Elezen that do not have a more specific character collection associated.", + CollectionType.MalePlainsfolkNpc => + "This collection applies to all male non-player character Plainsfolk Lalafell that do not have a more specific character collection associated.", + CollectionType.MaleDunesfolkNpc => + "This collection applies to all male non-player character Dunesfolk Lalafell that do not have a more specific character collection associated.", + CollectionType.MaleSeekerOfTheSunNpc => + "This collection applies to all male non-player character Seekers of the Sun that do not have a more specific character collection associated.", + CollectionType.MaleKeeperOfTheMoonNpc => + "This collection applies to all male non-player character Keepers of the Moon that do not have a more specific character collection associated.", + CollectionType.MaleSeawolfNpc => + "This collection applies to all male non-player character Sea Wolf Roegadyn that do not have a more specific character collection associated.", + CollectionType.MaleHellsguardNpc => + "This collection applies to all male non-player character Hellsguard Roegadyn that do not have a more specific character collection associated.", + CollectionType.MaleRaenNpc => + "This collection applies to all male non-player character Raen Au Ra that do not have a more specific character collection associated.", + CollectionType.MaleXaelaNpc => + "This collection applies to all male non-player character Xaela Au Ra that do not have a more specific character collection associated.", + CollectionType.MaleHelionNpc => + "This collection applies to all male non-player character Helion Hrothgar that do not have a more specific character collection associated.", + CollectionType.MaleLostNpc => + "This collection applies to all male non-player character Lost Hrothgar that do not have a more specific character collection associated.", + CollectionType.MaleRavaNpc => + "This collection applies to all male non-player character Rava Viera that do not have a more specific character collection associated.", + CollectionType.MaleVeenaNpc => + "This collection applies to all male non-player character Veena Viera that do not have a more specific character collection associated.", + CollectionType.FemalePlayerCharacter => + "This collection applies to all female player characters that do not have a more specific character or racial collections associated.", + CollectionType.FemaleNonPlayerCharacter => + "This collection applies to all human female non-player characters except those explicitly named. It takes precedence before the default and racial collections.", + CollectionType.FemaleMidlander => + "This collection applies to all female player character Midlander Hyur that do not have a more specific character collection associated.", + CollectionType.FemaleHighlander => + "This collection applies to all female player character Highlander Hyur that do not have a more specific character collection associated.", + CollectionType.FemaleWildwood => + "This collection applies to all female player character Wildwood Elezen that do not have a more specific character collection associated.", + CollectionType.FemaleDuskwight => + "This collection applies to all female player character Duskwight Elezen that do not have a more specific character collection associated.", + CollectionType.FemalePlainsfolk => + "This collection applies to all female player character Plainsfolk Lalafell that do not have a more specific character collection associated.", + CollectionType.FemaleDunesfolk => + "This collection applies to all female player character Dunesfolk Lalafell that do not have a more specific character collection associated.", + CollectionType.FemaleSeekerOfTheSun => + "This collection applies to all female player character Seekers of the Sun that do not have a more specific character collection associated.", + CollectionType.FemaleKeeperOfTheMoon => + "This collection applies to all female player character Keepers of the Moon that do not have a more specific character collection associated.", + CollectionType.FemaleSeawolf => + "This collection applies to all female player character Sea Wolf Roegadyn that do not have a more specific character collection associated.", + CollectionType.FemaleHellsguard => + "This collection applies to all female player character Hellsguard Roegadyn that do not have a more specific character collection associated.", + CollectionType.FemaleRaen => + "This collection applies to all female player character Raen Au Ra that do not have a more specific character collection associated.", + CollectionType.FemaleXaela => + "This collection applies to all female player character Xaela Au Ra that do not have a more specific character collection associated.", + CollectionType.FemaleHelion => + "This collection applies to all female player character Helion Hrothgar that do not have a more specific character collection associated.", + CollectionType.FemaleLost => + "This collection applies to all female player character Lost Hrothgar that do not have a more specific character collection associated.", + CollectionType.FemaleRava => + "This collection applies to all female player character Rava Viera that do not have a more specific character collection associated.", + CollectionType.FemaleVeena => + "This collection applies to all female player character Veena Viera that do not have a more specific character collection associated.", + CollectionType.FemaleMidlanderNpc => + "This collection applies to all female non-player character Midlander Hyur that do not have a more specific character collection associated.", + CollectionType.FemaleHighlanderNpc => + "This collection applies to all female non-player character Highlander Hyur that do not have a more specific character collection associated.", + CollectionType.FemaleWildwoodNpc => + "This collection applies to all female non-player character Wildwood Elezen that do not have a more specific character collection associated.", + CollectionType.FemaleDuskwightNpc => + "This collection applies to all female non-player character Duskwight Elezen that do not have a more specific character collection associated.", + CollectionType.FemalePlainsfolkNpc => + "This collection applies to all female non-player character Plainsfolk Lalafell that do not have a more specific character collection associated.", + CollectionType.FemaleDunesfolkNpc => + "This collection applies to all female non-player character Dunesfolk Lalafell that do not have a more specific character collection associated.", + CollectionType.FemaleSeekerOfTheSunNpc => + "This collection applies to all female non-player character Seekers of the Sun that do not have a more specific character collection associated.", + CollectionType.FemaleKeeperOfTheMoonNpc => + "This collection applies to all female non-player character Keepers of the Moon that do not have a more specific character collection associated.", + CollectionType.FemaleSeawolfNpc => + "This collection applies to all female non-player character Sea Wolf Roegadyn that do not have a more specific character collection associated.", + CollectionType.FemaleHellsguardNpc => + "This collection applies to all female non-player character Hellsguard Roegadyn that do not have a more specific character collection associated.", + CollectionType.FemaleRaenNpc => + "This collection applies to all female non-player character Raen Au Ra that do not have a more specific character collection associated.", + CollectionType.FemaleXaelaNpc => + "This collection applies to all female non-player character Xaela Au Ra that do not have a more specific character collection associated.", + CollectionType.FemaleHelionNpc => + "This collection applies to all female non-player character Helion Hrothgar that do not have a more specific character collection associated.", + CollectionType.FemaleLostNpc => + "This collection applies to all female non-player character Lost Hrothgar that do not have a more specific character collection associated.", + CollectionType.FemaleRavaNpc => + "This collection applies to all female non-player character Rava Viera that do not have a more specific character collection associated.", + CollectionType.FemaleVeenaNpc => + "This collection applies to all female non-player character Veena Viera that do not have a more specific character collection associated.", + _ => string.Empty, + }; +} \ No newline at end of file diff --git a/Penumbra/Collections/IndividualCollections.Access.cs b/Penumbra/Collections/Manager/IndividualCollections.Access.cs similarity index 99% rename from Penumbra/Collections/IndividualCollections.Access.cs rename to Penumbra/Collections/Manager/IndividualCollections.Access.cs index 0b43baf3..32e7fd17 100644 --- a/Penumbra/Collections/IndividualCollections.Access.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Access.cs @@ -7,7 +7,7 @@ using Dalamud.Game.ClientState.Objects.Types; using Penumbra.GameData.Actors; using Penumbra.String; -namespace Penumbra.Collections; +namespace Penumbra.Collections.Manager; public sealed partial class IndividualCollections : IReadOnlyList< (string DisplayName, ModCollection Collection) > { diff --git a/Penumbra/Collections/Manager/IndividualCollections.Files.cs b/Penumbra/Collections/Manager/IndividualCollections.Files.cs new file mode 100644 index 00000000..bc0d5737 --- /dev/null +++ b/Penumbra/Collections/Manager/IndividualCollections.Files.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Interface.Internal.Notifications; +using Newtonsoft.Json.Linq; +using Penumbra.Collections.Manager; +using Penumbra.GameData.Actors; +using Penumbra.String; + +namespace Penumbra.Collections.Manager; + +public partial class IndividualCollections +{ + public JArray ToJObject() + { + var ret = new JArray(); + foreach (var (name, identifiers, collection) in Assignments) + { + var tmp = identifiers[0].ToJson(); + tmp.Add("Collection", collection.Name); + tmp.Add("Display", name); + ret.Add(tmp); + } + + return ret; + } + + public bool ReadJObject(JArray? obj, CollectionStorage storage) + { + if (obj == null) + return true; + + var changes = false; + foreach (var data in obj) + { + try + { + var identifier = _actorManager.FromJson(data as JObject); + var group = GetGroup(identifier); + if (group.Length == 0 || group.Any(i => !i.IsValid)) + { + changes = true; + Penumbra.ChatService.NotificationMessage("Could not load an unknown individual collection, removed.", "Load Failure", + NotificationType.Warning); + continue; + } + + var collectionName = data["Collection"]?.ToObject() ?? string.Empty; + if (collectionName.Length == 0 || !storage.ByName(collectionName, out var collection)) + { + changes = true; + Penumbra.ChatService.NotificationMessage( + $"Could not load the collection \"{collectionName}\" as individual collection for {identifier}, set to None.", + "Load Failure", + NotificationType.Warning); + continue; + } + + if (!Add(group, collection)) + { + changes = true; + Penumbra.ChatService.NotificationMessage($"Could not add an individual collection for {identifier}, removed.", + "Load Failure", + NotificationType.Warning); + } + } + catch (Exception e) + { + changes = true; + Penumbra.ChatService.NotificationMessage($"Could not load an unknown individual collection, removed:\n{e}", "Load Failure", + NotificationType.Error); + } + } + + return changes; + } + + internal void Migrate0To1(Dictionary old) + { + static bool FindDataId(string name, IReadOnlyDictionary data, out uint dataId) + { + var kvp = data.FirstOrDefault(kvp => kvp.Value.Equals(name, StringComparison.OrdinalIgnoreCase), + new KeyValuePair(uint.MaxValue, string.Empty)); + dataId = kvp.Key; + return kvp.Value.Length > 0; + } + + foreach (var (name, collection) in old) + { + var kind = ObjectKind.None; + var lowerName = name.ToLowerInvariant(); + // Prefer matching NPC names, fewer false positives than preferring players. + if (FindDataId(lowerName, _actorManager.Data.Companions, out var dataId)) + kind = ObjectKind.Companion; + else if (FindDataId(lowerName, _actorManager.Data.Mounts, out dataId)) + kind = ObjectKind.MountType; + else if (FindDataId(lowerName, _actorManager.Data.BNpcs, out dataId)) + kind = ObjectKind.BattleNpc; + else if (FindDataId(lowerName, _actorManager.Data.ENpcs, out dataId)) + kind = ObjectKind.EventNpc; + + var identifier = _actorManager.CreateNpc(kind, dataId); + if (identifier.IsValid) + { + // If the name corresponds to a valid npc, add it as a group. If this fails, notify users. + var group = GetGroup(identifier); + var ids = string.Join(", ", group.Select(i => i.DataId.ToString())); + if (Add($"{_actorManager.Data.ToName(kind, dataId)} ({kind.ToName()})", group, collection)) + Penumbra.Log.Information($"Migrated {name} ({kind.ToName()}) to NPC Identifiers [{ids}]."); + else + Penumbra.ChatService.NotificationMessage( + $"Could not migrate {name} ({collection.AnonymizedName}) which was assumed to be a {kind.ToName()} with IDs [{ids}], please look through your individual collections.", + "Migration Failure", NotificationType.Error); + } + // If it is not a valid NPC name, check if it can be a player name. + else if (ActorManager.VerifyPlayerName(name)) + { + identifier = _actorManager.CreatePlayer(ByteString.FromStringUnsafe(name, false), ushort.MaxValue); + var shortName = string.Join(" ", name.Split().Select(n => $"{n[0]}.")); + // Try to migrate the player name without logging full names. + if (Add($"{name} ({_actorManager.Data.ToWorldName(identifier.HomeWorld)})", new[] + { + identifier, + }, collection)) + Penumbra.Log.Information($"Migrated {shortName} ({collection.AnonymizedName}) to Player Identifier."); + else + Penumbra.ChatService.NotificationMessage( + $"Could not migrate {shortName} ({collection.AnonymizedName}), please look through your individual collections.", + "Migration Failure", NotificationType.Error); + } + else + { + Penumbra.ChatService.NotificationMessage( + $"Could not migrate {name} ({collection.AnonymizedName}), which can not be a player name nor is it a known NPC name, please look through your individual collections.", + "Migration Failure", NotificationType.Error); + } + } + } +} diff --git a/Penumbra/Collections/IndividualCollections.cs b/Penumbra/Collections/Manager/IndividualCollections.cs similarity index 99% rename from Penumbra/Collections/IndividualCollections.cs rename to Penumbra/Collections/Manager/IndividualCollections.cs index e5de838a..28059ecf 100644 --- a/Penumbra/Collections/IndividualCollections.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.cs @@ -8,7 +8,7 @@ using Penumbra.GameData.Actors; using Penumbra.Services; using Penumbra.String; -namespace Penumbra.Collections; +namespace Penumbra.Collections.Manager; public sealed partial class IndividualCollections { @@ -234,7 +234,7 @@ public sealed partial class IndividualCollections => identifier.IsValid ? Index(DisplayString(identifier)) : -1; private string DisplayString(ActorIdentifier identifier) - { + { return identifier.Type switch { IdentifierType.Player => $"{identifier.PlayerName} ({_actorManager.Data.ToWorldName(identifier.HomeWorld)})", diff --git a/Penumbra/Collections/Manager/InheritanceManager.cs b/Penumbra/Collections/Manager/InheritanceManager.cs new file mode 100644 index 00000000..06227fdf --- /dev/null +++ b/Penumbra/Collections/Manager/InheritanceManager.cs @@ -0,0 +1,68 @@ +using System; +using Dalamud.Interface.Internal.Notifications; +using Penumbra.Services; +using Penumbra.Util; + +namespace Penumbra.Collections.Manager; + +public class InheritanceManager : IDisposable +{ + private readonly CollectionStorage _storage; + private readonly CommunicatorService _communicator; + private readonly SaveService _saveService; + + public InheritanceManager(CollectionStorage storage, SaveService saveService, CommunicatorService communicator) + { + _storage = storage; + _saveService = saveService; + _communicator = communicator; + + ApplyInheritances(); + _communicator.CollectionChange.Subscribe(OnCollectionChange); + } + + public void Dispose() + { + _communicator.CollectionChange.Unsubscribe(OnCollectionChange); + } + + /// + /// Inheritances can not be setup before all collections are read, + /// so this happens after reading the collections in the constructor, consuming the stored strings. + /// + private void ApplyInheritances() + { + foreach (var (collection, inheritances, changes) in _storage.ConsumeInheritanceNames()) + { + var localChanges = changes; + foreach (var subCollection in inheritances) + { + if (collection.AddInheritance(subCollection, false)) + continue; + + localChanges = true; + Penumbra.ChatService.NotificationMessage($"{collection.Name} can not inherit from {subCollection.Name}, removed.", "Warning", + NotificationType.Warning); + } + + if (localChanges) + _saveService.ImmediateSave(collection); + } + } + + private void OnCollectionChange(CollectionType collectionType, ModCollection? old, ModCollection? newCollection, string _3) + { + if (collectionType is not CollectionType.Inactive || old == null) + return; + + foreach (var inheritance in old.Inheritance) + old.ClearSubscriptions(inheritance); + + foreach (var c in _storage) + { + var inheritedIdx = c._inheritance.IndexOf(old); + if (inheritedIdx >= 0) + c.RemoveInheritance(inheritedIdx); + } + } +} diff --git a/Penumbra/Api/TempCollectionManager.cs b/Penumbra/Collections/Manager/TempCollectionManager.cs similarity index 86% rename from Penumbra/Api/TempCollectionManager.cs rename to Penumbra/Collections/Manager/TempCollectionManager.cs index 455751c6..0eed53d6 100644 --- a/Penumbra/Api/TempCollectionManager.cs +++ b/Penumbra/Collections/Manager/TempCollectionManager.cs @@ -2,13 +2,13 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; -using Penumbra.Collections; +using Penumbra.Api; using Penumbra.GameData.Actors; using Penumbra.Mods; using Penumbra.Services; using Penumbra.String; -namespace Penumbra.Api; +namespace Penumbra.Collections.Manager; public class TempCollectionManager : IDisposable { @@ -16,19 +16,21 @@ public class TempCollectionManager : IDisposable public readonly IndividualCollections Collections; private readonly CommunicatorService _communicator; + private readonly CollectionStorage _storage; private readonly Dictionary _customCollections = new(); - public TempCollectionManager(CommunicatorService communicator, IndividualCollections collections) + public TempCollectionManager(CommunicatorService communicator, ActorService actors, CollectionStorage storage) { _communicator = communicator; - Collections = collections; + _storage = storage; + Collections = new IndividualCollections(actors); - _communicator.TemporaryGlobalModChange.Event += OnGlobalModChange; + _communicator.TemporaryGlobalModChange.Subscribe(OnGlobalModChange); } public void Dispose() { - _communicator.TemporaryGlobalModChange.Event -= OnGlobalModChange; + _communicator.TemporaryGlobalModChange.Unsubscribe(OnGlobalModChange); } private void OnGlobalModChange(TemporaryMod mod, bool created, bool removed) @@ -45,7 +47,7 @@ public class TempCollectionManager : IDisposable public string CreateTemporaryCollection(string name) { - if (Penumbra.CollectionManager.ByName(name, out _)) + if (_storage.ByName(name, out _)) return string.Empty; if (GlobalChangeCounter == int.MaxValue) diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 355f17b3..3dba6903 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -40,7 +40,7 @@ public partial class ModCollection // Force an update with metadata for this cache. internal void ForceCacheUpdate() - => CalculateEffectiveFileList(this == Penumbra.CollectionManager.Default); + => CalculateEffectiveFileList(this == Penumbra.CollectionManager.Active.Default); // Handle temporary mods for this collection. public void Apply(TemporaryMod tempMod, bool created) diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 7c7fa08a..4a3c008c 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -20,7 +20,7 @@ public record ModConflicts( IMod Mod2, List< object > Conflicts, bool HasPriorit /// The Cache contains all required temporary data to use a collection. /// It will only be setup if a collection gets activated in any way. /// -internal class ModCollectionCache : IDisposable +public class ModCollectionCache : IDisposable { private readonly ModCollection _collection; private readonly SortedList< string, (SingleArray< IMod >, object?) > _changedItems = new(); @@ -175,7 +175,7 @@ internal class ModCollectionCache : IDisposable break; case ModSettingChange.MultiInheritance: case ModSettingChange.MultiEnableState: - FullRecalculation(_collection == Penumbra.CollectionManager.Default); + FullRecalculation(_collection == Penumbra.CollectionManager.Active.Default); break; } } @@ -183,7 +183,7 @@ internal class ModCollectionCache : IDisposable // Inheritance changes are too big to check for relevance, // just recompute everything. private void OnInheritanceChange( bool _ ) - => FullRecalculation(_collection == Penumbra.CollectionManager.Default); + => FullRecalculation(_collection == Penumbra.CollectionManager.Active.Default); public void FullRecalculation(bool isDefault) { @@ -269,7 +269,7 @@ internal class ModCollectionCache : IDisposable if( addMetaChanges ) { ++_collection.ChangeCounter; - if( _collection == Penumbra.CollectionManager.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) + if( _collection == Penumbra.CollectionManager.Active.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) { Penumbra.ResidentResources.Reload(); MetaManipulations.SetFiles(); @@ -327,7 +327,7 @@ internal class ModCollectionCache : IDisposable AddMetaFiles(); } - if( _collection == Penumbra.CollectionManager.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) + if( _collection == Penumbra.CollectionManager.Active.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) { Penumbra.ResidentResources.Reload(); MetaManipulations.SetFiles(); diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 6215dc03..67913760 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -16,7 +16,7 @@ namespace Penumbra.Collections; public partial class ModCollection { public const int CurrentVersion = 1; - public const string DefaultCollection = "Default"; + public const string DefaultCollectionName = "Default"; public const string EmptyCollection = "None"; public static readonly ModCollection Empty = CreateEmpty(); @@ -100,11 +100,6 @@ public partial class ModCollection public ModCollection Duplicate(string name) => new(name, this); - // Check if a name is valid to use for a collection. - // Does not check for uniqueness. - public static bool IsValidName(string name) - => name.Length > 0 && name.All(c => !c.IsInvalidAscii() && c is not '|' && !c.IsInvalidInPath()); - // Remove all settings for not currently-installed mods. public void CleanUnavailableSettings() { diff --git a/Penumbra/Collections/ResolveData.cs b/Penumbra/Collections/ResolveData.cs index efabaaf2..c604d572 100644 --- a/Penumbra/Collections/ResolveData.cs +++ b/Penumbra/Collections/ResolveData.cs @@ -1,7 +1,4 @@ using System; -using System.Linq; -using FFXIVClientStructs.FFXIV.Client.Game.Object; -using Penumbra.GameData.Actors; namespace Penumbra.Collections; @@ -14,7 +11,7 @@ public readonly struct ResolveData public ModCollection ModCollection => _modCollection ?? ModCollection.Empty; - public readonly IntPtr AssociatedGameObject; + public readonly nint AssociatedGameObject; public bool Valid => _modCollection != null; @@ -22,17 +19,17 @@ public readonly struct ResolveData public ResolveData() { _modCollection = null!; - AssociatedGameObject = IntPtr.Zero; + AssociatedGameObject = nint.Zero; } - public ResolveData(ModCollection collection, IntPtr gameObject) + public ResolveData(ModCollection collection, nint gameObject) { _modCollection = collection; AssociatedGameObject = gameObject; } public ResolveData(ModCollection collection) - : this(collection, IntPtr.Zero) + : this(collection, nint.Zero) { } public override string ToString() @@ -44,9 +41,9 @@ public static class ResolveDataExtensions public static ResolveData ToResolveData(this ModCollection collection) => new(collection); - public static ResolveData ToResolveData(this ModCollection collection, IntPtr ptr) + public static ResolveData ToResolveData(this ModCollection collection, nint ptr) => new(collection, ptr); public static unsafe ResolveData ToResolveData(this ModCollection collection, void* ptr) - => new(collection, (IntPtr)ptr); + => new(collection, (nint)ptr); } diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 44415ff9..96acda5f 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -8,6 +8,7 @@ using Dalamud.Game.Text.SeStringHandling; using ImGuiNET; using Penumbra.Api.Enums; using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.Interop.Services; using Penumbra.Mods; @@ -291,7 +292,7 @@ public class CommandHandler : IDisposable } } - var oldCollection = _collectionManager.ByType(type, identifier); + var oldCollection = _collectionManager.Active.ByType(type, identifier); if (collection == oldCollection) { _chat.Print(collection == null @@ -300,30 +301,30 @@ public class CommandHandler : IDisposable return false; } - var individualIndex = _collectionManager.Individuals.Index(identifier); + var individualIndex = _collectionManager.Active.Individuals.Index(identifier); if (oldCollection == null) { if (type.IsSpecial()) { - _collectionManager.CreateSpecialCollection(type); + _collectionManager.Active.CreateSpecialCollection(type); } else if (identifier.IsValid) { - var identifiers = _collectionManager.Individuals.GetGroup(identifier); - individualIndex = _collectionManager.Individuals.Count; - _collectionManager.CreateIndividualCollection(identifiers); + var identifiers = _collectionManager.Active.Individuals.GetGroup(identifier); + individualIndex = _collectionManager.Active.Individuals.Count; + _collectionManager.Active.CreateIndividualCollection(identifiers); } } else if (collection == null) { if (type.IsSpecial()) { - _collectionManager.RemoveSpecialCollection(type); + _collectionManager.Active.RemoveSpecialCollection(type); } else if (individualIndex >= 0) { - _collectionManager.RemoveIndividualCollection(individualIndex); + _collectionManager.Active.RemoveIndividualCollection(individualIndex); } else { @@ -337,7 +338,7 @@ public class CommandHandler : IDisposable return true; } - _collectionManager.SetCollection(collection!, type, individualIndex); + _collectionManager.Active.SetCollection(collection!, type, individualIndex); Print($"Assigned {collection!.Name} as {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}." : ".")}"); return true; } @@ -454,15 +455,14 @@ public class CommandHandler : IDisposable collection = string.Equals(lowerName, ModCollection.Empty.Name, StringComparison.OrdinalIgnoreCase) ? ModCollection.Empty - : _collectionManager[lowerName]; - if (collection == null) - { - _chat.Print(new SeStringBuilder().AddText("The collection ").AddRed(collectionName, true).AddText(" does not exist.") - .BuiltString); - return false; - } + : _collectionManager.Storage.ByName(lowerName, out var c) ? c : null; + if (collection != null) + return true; + + _chat.Print(new SeStringBuilder().AddText("The collection ").AddRed(collectionName, true).AddText(" does not exist.") + .BuiltString); + return false; - return true; } private static bool? ParseTrueFalseToggle(string value) diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index 65fc0771..6b314ca2 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -7,8 +7,8 @@ using Dalamud.Game.Gui; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using Lumina.Excel.GeneratedSheets; using OtterGui; -using Penumbra.Api; using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; using Penumbra.Services; @@ -61,15 +61,15 @@ public unsafe class CollectionResolver using var performance = _performance.Measure(PerformanceType.IdentifyCollection); var gameObject = (GameObject*)(_clientState.LocalPlayer?.Address ?? nint.Zero); if (gameObject == null) - return _collectionManager.ByType(CollectionType.Yourself) - ?? _collectionManager.Default; + return _collectionManager.Active.ByType(CollectionType.Yourself) + ?? _collectionManager.Active.Default; var player = _actors.AwaitedService.GetCurrentPlayer(); var _ = false; return CollectionByIdentifier(player) ?? CheckYourself(player, gameObject) ?? CollectionByAttributes(gameObject, ref _) - ?? _collectionManager.Default; + ?? _collectionManager.Active.Default; } /// Identify the correct collection for a game object. @@ -78,7 +78,7 @@ public unsafe class CollectionResolver using var t = _performance.Measure(PerformanceType.IdentifyCollection); if (gameObject == null) - return _collectionManager.Default.ToResolveData(); + return _collectionManager.Active.Default.ToResolveData(); try { @@ -96,7 +96,7 @@ public unsafe class CollectionResolver catch (Exception ex) { Penumbra.Log.Error($"Error identifying collection:\n{ex}"); - return _collectionManager.Default.ToResolveData(gameObject); + return _collectionManager.Active.Default.ToResolveData(gameObject); } } @@ -137,9 +137,9 @@ public unsafe class CollectionResolver } var notYetReady = false; - var collection = _collectionManager.ByType(CollectionType.Yourself) + var collection = _collectionManager.Active.ByType(CollectionType.Yourself) ?? CollectionByAttributes(gameObject, ref notYetReady) - ?? _collectionManager.Default; + ?? _collectionManager.Active.Default; ret = notYetReady ? collection.ToResolveData(gameObject) : _cache.Set(collection, ActorIdentifier.Invalid, gameObject); return true; } @@ -156,9 +156,9 @@ public unsafe class CollectionResolver var player = _actors.AwaitedService.GetCurrentPlayer(); var notYetReady = false; var collection = (player.IsValid ? CollectionByIdentifier(player) : null) - ?? _collectionManager.ByType(CollectionType.Yourself) + ?? _collectionManager.Active.ByType(CollectionType.Yourself) ?? CollectionByAttributes(gameObject, ref notYetReady) - ?? _collectionManager.Default; + ?? _collectionManager.Active.Default; ret = notYetReady ? collection.ToResolveData(gameObject) : _cache.Set(collection, ActorIdentifier.Invalid, gameObject); return true; } @@ -172,7 +172,7 @@ public unsafe class CollectionResolver var identifier = _actors.AwaitedService.FromObject(gameObject, out var owner, true, false, false); if (identifier.Type is IdentifierType.Special) { - (identifier, var type) = _collectionManager.Individuals.ConvertSpecialIdentifier(identifier); + (identifier, var type) = _collectionManager.Active.Individuals.ConvertSpecialIdentifier(identifier); if (_config.UseNoModsInInspect && type == IndividualCollections.SpecialResult.Inspect) return _cache.Set(ModCollection.Empty, identifier, gameObject); } @@ -182,7 +182,7 @@ public unsafe class CollectionResolver ?? CheckYourself(identifier, gameObject) ?? CollectionByAttributes(gameObject, ref notYetReady) ?? CheckOwnedCollection(identifier, owner, ref notYetReady) - ?? _collectionManager.Default; + ?? _collectionManager.Active.Default; return notYetReady ? collection.ToResolveData(gameObject) : _cache.Set(collection, identifier, gameObject); } @@ -190,7 +190,7 @@ public unsafe class CollectionResolver /// Check both temporary and permanent character collections. Temporary first. private ModCollection? CollectionByIdentifier(ActorIdentifier identifier) => _tempCollections.Collections.TryGetCollection(identifier, out var collection) - || _collectionManager.Individuals.TryGetCollection(identifier, out collection) + || _collectionManager.Active.Individuals.TryGetCollection(identifier, out collection) ? collection : null; @@ -200,7 +200,7 @@ public unsafe class CollectionResolver if (actor->ObjectIndex == 0 || _cutscenes.GetParentIndex(actor->ObjectIndex) == 0 || identifier.Equals(_actors.AwaitedService.GetCurrentPlayer())) - return _collectionManager.ByType(CollectionType.Yourself); + return _collectionManager.Active.ByType(CollectionType.Yourself); return null; } @@ -225,8 +225,8 @@ public unsafe class CollectionResolver var bodyType = character->CustomizeData[2]; var collection = bodyType switch { - 3 => _collectionManager.ByType(CollectionType.NonPlayerElderly), - 4 => _collectionManager.ByType(CollectionType.NonPlayerChild), + 3 => _collectionManager.Active.ByType(CollectionType.NonPlayerElderly), + 4 => _collectionManager.Active.ByType(CollectionType.NonPlayerChild), _ => null, }; if (collection != null) @@ -237,8 +237,8 @@ public unsafe class CollectionResolver var isNpc = actor->ObjectKind != (byte)ObjectKind.Player; var type = CollectionTypeExtensions.FromParts(race, gender, isNpc); - collection = _collectionManager.ByType(type); - collection ??= _collectionManager.ByType(CollectionTypeExtensions.FromParts(gender, isNpc)); + collection = _collectionManager.Active.ByType(type); + collection ??= _collectionManager.Active.ByType(CollectionTypeExtensions.FromParts(gender, isNpc)); return collection; } diff --git a/Penumbra/Interop/PathResolving/DrawObjectState.cs b/Penumbra/Interop/PathResolving/DrawObjectState.cs index e5ce7026..8273aed3 100644 --- a/Penumbra/Interop/PathResolving/DrawObjectState.cs +++ b/Penumbra/Interop/PathResolving/DrawObjectState.cs @@ -1,17 +1,13 @@ using Dalamud.Hooking; using Dalamud.Utility.Signatures; -using Penumbra.Collections; using System; using System.Collections; using System.Collections.Generic; using System.Threading; using Dalamud.Game.ClientState.Objects; using FFXIVClientStructs.FFXIV.Client.Game.Object; -using Penumbra.Api; using Penumbra.GameData; -using Penumbra.GameData.Enums; using Penumbra.Interop.Services; -using Penumbra.String.Classes; using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object; namespace Penumbra.Interop.PathResolving; diff --git a/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs b/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs index 0d60e72b..58ae0d92 100644 --- a/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs +++ b/Penumbra/Interop/PathResolving/IdentifiedCollectionCache.cs @@ -5,6 +5,7 @@ using Dalamud.Game.ClientState; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Object; using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.Interop.Services; using Penumbra.Services; @@ -25,7 +26,7 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint A _communicator = communicator; _events = events; - _communicator.CollectionChange.Event += CollectionChangeClear; + _communicator.CollectionChange.Subscribe(CollectionChangeClear); _clientState.TerritoryChanged += TerritoryClear; _events.CharacterDestructor += OnCharacterDestruct; } @@ -61,7 +62,7 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint A public void Dispose() { - _communicator.CollectionChange.Event -= CollectionChangeClear; + _communicator.CollectionChange.Unsubscribe(CollectionChangeClear); _clientState.TerritoryChanged -= TerritoryClear; _events.CharacterDestructor -= OnCharacterDestruct; } diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index 734971c5..a05497be 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -1,8 +1,8 @@ using System; using System.Diagnostics.CodeAnalysis; using FFXIVClientStructs.FFXIV.Client.System.Resource; -using Penumbra.Api; using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.GameData.Enums; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.Structs; @@ -44,7 +44,7 @@ public class PathResolver : IDisposable /// Obtain a temporary or permanent collection by name. public bool CollectionByName(string name, [NotNullWhen(true)] out ModCollection? collection) - => _tempCollections.CollectionByName(name, out collection) || _collectionManager.ByName(name, out collection); + => _tempCollections.CollectionByName(name, out collection) || _collectionManager.Storage.ByName(name, out collection); /// Try to resolve the given game path to the replaced path. public (FullPath?, ResolveData) ResolvePath(Utf8GamePath path, ResourceCategory category, ResourceType resourceType) @@ -57,8 +57,8 @@ public class PathResolver : IDisposable return category switch { // Only Interface collection. - ResourceCategory.Ui => (_collectionManager.Interface.ResolvePath(path), - _collectionManager.Interface.ToResolveData()), + ResourceCategory.Ui => (_collectionManager.Active.Interface.ResolvePath(path), + _collectionManager.Active.Interface.ToResolveData()), // Never allow changing scripts. ResourceCategory.UiScript => (null, ResolveData.Invalid), ResourceCategory.GameScript => (null, ResolveData.Invalid), @@ -93,7 +93,7 @@ public class PathResolver : IDisposable || _animationHookService.HandleFiles(type, gamePath, out resolveData) || _metaState.HandleDecalFile(type, gamePath, out resolveData); if (!nonDefault || !resolveData.Valid) - resolveData = _collectionManager.Default.ToResolveData(); + resolveData = _collectionManager.Active.Default.ToResolveData(); // Resolve using character/default collection first, otherwise forced, as usual. var resolved = resolveData.ModCollection.ResolvePath(gamePath); @@ -115,8 +115,8 @@ public class PathResolver : IDisposable /// Use the default method of path replacement. private (FullPath?, ResolveData) DefaultResolver(Utf8GamePath path) { - var resolved = _collectionManager.Default.ResolvePath(path); - return (resolved, _collectionManager.Default.ToResolveData()); + var resolved = _collectionManager.Active.Default.ResolvePath(path); + return (resolved, _collectionManager.Active.Default.ToResolveData()); } /// After loading an IMC file, replace its contents with the modded IMC file. diff --git a/Penumbra/Interop/Services/CharacterUtility.List.cs b/Penumbra/Interop/Services/CharacterUtility.List.cs index 1fc33efb..abe024af 100644 --- a/Penumbra/Interop/Services/CharacterUtility.List.cs +++ b/Penumbra/Interop/Services/CharacterUtility.List.cs @@ -97,7 +97,7 @@ public unsafe partial class CharacterUtility => SetResourceInternal(_defaultResourceData, _defaultResourceSize); private void SetResourceToDefaultCollection() - => Penumbra.CollectionManager.Default.SetMetaFile(GlobalMetaIndex); + => Penumbra.CollectionManager.Active.Default.SetMetaFile(GlobalMetaIndex); public void Dispose() { diff --git a/Penumbra/Meta/Manager/MetaManager.cs b/Penumbra/Meta/Manager/MetaManager.cs index 1eb192fc..bd3a6086 100644 --- a/Penumbra/Meta/Manager/MetaManager.cs +++ b/Penumbra/Meta/Manager/MetaManager.cs @@ -153,7 +153,7 @@ public partial class MetaManager : IDisposable, IEnumerable< KeyValuePair< MetaM : 0; } - if( Penumbra.CollectionManager.Default == _collection ) + if( Penumbra.CollectionManager.Active.Default == _collection ) { SetFiles(); Penumbra.ResidentResources.Reload(); diff --git a/Penumbra/Mods/Manager/ExportManager.cs b/Penumbra/Mods/Manager/ExportManager.cs index a59315d6..3d091105 100644 --- a/Penumbra/Mods/Manager/ExportManager.cs +++ b/Penumbra/Mods/Manager/ExportManager.cs @@ -21,7 +21,7 @@ public class ExportManager : IDisposable _communicator = communicator; _modManager = modManager; UpdateExportDirectory(_config.ExportDirectory, false); - _communicator.ModPathChanged.Event += OnModPathChange; + _communicator.ModPathChanged.Subscribe(OnModPathChange); } /// @@ -76,7 +76,7 @@ public class ExportManager : IDisposable } public void Dispose() - => _communicator.ModPathChanged.Event -= OnModPathChange; + => _communicator.ModPathChanged.Unsubscribe(OnModPathChange); /// Automatically migrate the backup file to the new name if any exists. private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index 85637707..a133e9ed 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -26,10 +26,10 @@ public class ModCacheManager : IDisposable, IReadOnlyList _identifier = identifier; _modManager = modManager; - _communicator.ModOptionChanged.Event += OnModOptionChange; - _communicator.ModPathChanged.Event += OnModPathChange; - _communicator.ModDataChanged.Event += OnModDataChange; - _communicator.ModDiscoveryFinished.Event += OnModDiscoveryFinished; + _communicator.ModOptionChanged.Subscribe(OnModOptionChange); + _communicator.ModPathChanged.Subscribe(OnModPathChange); + _communicator.ModDataChanged.Subscribe(OnModDataChange); + _communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished); if (!identifier.Valid) identifier.FinishedCreation += OnIdentifierCreation; OnModDiscoveryFinished(); @@ -51,10 +51,10 @@ public class ModCacheManager : IDisposable, IReadOnlyList public void Dispose() { - _communicator.ModOptionChanged.Event -= OnModOptionChange; - _communicator.ModPathChanged.Event -= OnModPathChange; - _communicator.ModDataChanged.Event -= OnModDataChange; - _communicator.ModDiscoveryFinished.Event -= OnModDiscoveryFinished; + _communicator.ModOptionChanged.Unsubscribe(OnModOptionChange); + _communicator.ModPathChanged.Unsubscribe(OnModPathChange); + _communicator.ModDataChanged.Unsubscribe(OnModDataChange); + _communicator.ModDiscoveryFinished.Unsubscribe(OnModDiscoveryFinished); } /// Compute the items changed by a given meta manipulation and put them into the changedItems dictionary. diff --git a/Penumbra/Mods/Manager/ModFileSystem.cs b/Penumbra/Mods/Manager/ModFileSystem.cs index 7f5d3070..2d5201ad 100644 --- a/Penumbra/Mods/Manager/ModFileSystem.cs +++ b/Penumbra/Mods/Manager/ModFileSystem.cs @@ -5,10 +5,11 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using OtterGui.Filesystem; +using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.Util; -namespace Penumbra.Mods.Manager; +namespace Penumbra.Mods; public sealed class ModFileSystem : FileSystem, IDisposable, ISavable { @@ -23,17 +24,17 @@ public sealed class ModFileSystem : FileSystem, IDisposable, ISavable _communicator = communicator; _files = files; Reload(); - Changed += OnChange; - _communicator.ModDiscoveryFinished.Event += Reload; - _communicator.ModDataChanged.Event += OnDataChange; - _communicator.ModPathChanged.Event += OnModPathChange; + Changed += OnChange; + _communicator.ModDiscoveryFinished.Subscribe(Reload); + _communicator.ModDataChanged.Subscribe(OnDataChange); + _communicator.ModPathChanged.Subscribe(OnModPathChange); } public void Dispose() { - _communicator.ModPathChanged.Event -= OnModPathChange; - _communicator.ModDiscoveryFinished.Event -= Reload; - _communicator.ModDataChanged.Event -= OnDataChange; + _communicator.ModPathChanged.Unsubscribe(OnModPathChange); + _communicator.ModDiscoveryFinished.Unsubscribe(Reload); + _communicator.ModDataChanged.Unsubscribe(OnDataChange); } public struct ImportDate : ISortMode diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 24fcd271..6265e3fd 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -29,8 +29,10 @@ using DalamudUtil = Dalamud.Utility.Util; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; using Penumbra.Services; using Penumbra.Interop.Services; -using Penumbra.Mods.Manager; - +using Penumbra.Mods.Manager; +using Penumbra.Collections.Manager; +using Penumbra.Mods; + namespace Penumbra; public class Penumbra : IDalamudPlugin @@ -183,7 +185,7 @@ public class Penumbra : IDalamudPlugin { if (CharacterUtility.Ready) { - CollectionManager.Default.SetFiles(); + CollectionManager.Active.Default.SetFiles(); ResidentResources.Reload(); RedrawService.RedrawAll(RedrawType.Redraw); } @@ -269,23 +271,23 @@ public class Penumbra : IDalamudPlugin + $"> **`Conflicts (Solved/Total): `** {c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority && x.Solved ? x.Conflicts.Count : 0)}/{c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority ? x.Conflicts.Count : 0)}\n"); sb.AppendLine("**Collections**"); - sb.Append($"> **`#Collections: `** {CollectionManager.Count - 1}\n"); + sb.Append($"> **`#Collections: `** {CollectionManager.Storage.Count - 1}\n"); sb.Append($"> **`#Temp Collections: `** {TempCollections.Count}\n"); - sb.Append($"> **`Active Collections: `** {CollectionManager.Count(c => c.HasCache)}\n"); - sb.Append($"> **`Base Collection: `** {CollectionManager.Default.AnonymizedName}\n"); - sb.Append($"> **`Interface Collection: `** {CollectionManager.Interface.AnonymizedName}\n"); - sb.Append($"> **`Selected Collection: `** {CollectionManager.Current.AnonymizedName}\n"); + sb.Append($"> **`Active Collections: `** {CollectionManager.Caches.Count}\n"); + sb.Append($"> **`Base Collection: `** {CollectionManager.Active.Default.AnonymizedName}\n"); + sb.Append($"> **`Interface Collection: `** {CollectionManager.Active.Interface.AnonymizedName}\n"); + sb.Append($"> **`Selected Collection: `** {CollectionManager.Active.Current.AnonymizedName}\n"); foreach (var (type, name, _) in CollectionTypeExtensions.Special) { - var collection = CollectionManager.ByType(type); + var collection = CollectionManager.Active.ByType(type); if (collection != null) sb.Append($"> **`{name,-30}`** {collection.AnonymizedName}\n"); } - foreach (var (name, id, collection) in CollectionManager.Individuals.Assignments) + foreach (var (name, id, collection) in CollectionManager.Active.Individuals.Assignments) sb.Append($"> **`{CharacterName(id[0], name),-30}`** {collection.AnonymizedName}\n"); - foreach (var collection in CollectionManager.Where(c => c.HasCache)) + foreach (var collection in CollectionManager.Caches.Active) PrintCollection(collection); return sb.ToString(); diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index 93abde46..f7e2da03 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -5,6 +5,7 @@ using OtterGui.Classes; using OtterGui.Log; using Penumbra.Api; using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.GameData; using Penumbra.GameData.Data; using Penumbra.Interop.ResourceLoading; @@ -85,7 +86,10 @@ public class PenumbraNew .AddSingleton(); // Add Collection Services - services.AddTransient() + services.AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton(); diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index fea11316..8a47ff40 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -1,97 +1,207 @@ using System; using System.IO; using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Util; namespace Penumbra.Services; +/// +/// Triggered whenever collection setup is changed. +/// +/// Parameter is the type of the changed collection. (Inactive or Temporary for additions or deletions) +/// Parameter is the old collection, or null on additions. +/// Parameter is the new collection, or null on deletions. +/// Parameter is the display name for Individual collections or an empty string otherwise. +/// +public sealed class CollectionChange : EventWrapper> +{ + public CollectionChange() + : base(nameof(CollectionChange)) + { } + + public void Invoke(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string displayName) + => Invoke(this, collectionType, oldCollection, newCollection, displayName); +} + +/// +/// Triggered whenever a temporary mod for all collections is changed. +/// +/// Parameter added, deleted or edited temporary mod. +/// Parameter is whether the mod was newly created. +/// Parameter is whether the mod was deleted. +/// +public sealed class TemporaryGlobalModChange : EventWrapper> +{ + public TemporaryGlobalModChange() + : base(nameof(TemporaryGlobalModChange)) + { } + + public void Invoke(TemporaryMod temporaryMod, bool newlyCreated, bool deleted) + => Invoke(this, temporaryMod, newlyCreated, deleted); +} + +/// +/// Triggered whenever a character base draw object is being created by the game. +/// +/// Parameter is the game object for which a draw object is created. +/// Parameter is the name of the applied collection. +/// Parameter is a pointer to the model id (an uint). +/// Parameter is a pointer to the customize array. +/// Parameter is a pointer to the equip data array. +/// +public sealed class CreatingCharacterBase : EventWrapper> +{ + public CreatingCharacterBase() + : base(nameof(CreatingCharacterBase)) + { } + + public void Invoke(nint gameObject, string appliedCollectionName, nint modelIdAddress, nint customizeArrayAddress, nint equipDataAddress) + => Invoke(this, gameObject, appliedCollectionName, modelIdAddress, customizeArrayAddress, equipDataAddress); +} + +/// +/// Parameter is the game object for which a draw object is created. +/// Parameter is the name of the applied collection. +/// Parameter is the created draw object. +/// +public sealed class CreatedCharacterBase : EventWrapper> +{ + public CreatedCharacterBase() + : base(nameof(CreatedCharacterBase)) + { } + + public void Invoke(nint gameObject, string appliedCollectionName, nint drawObject) + => Invoke(this, gameObject, appliedCollectionName, drawObject); +} + +/// +/// Triggered whenever mod meta data or local data is changed. +/// +/// Parameter is the type of data change for the mod, which can be multiple flags. +/// Parameter is the changed mod. +/// Parameter is the old name of the mod in case of a name change, and null otherwise. +/// +public sealed class ModDataChanged : EventWrapper> +{ + public ModDataChanged() + : base(nameof(ModDataChanged)) + { } + + public void Invoke(ModDataChangeType changeType, Mod mod, string? oldName) + => Invoke(this, changeType, mod, oldName); +} + +/// +/// Triggered whenever an option of a mod is changed inside the mod. +/// +/// Parameter is the type option change. +/// Parameter is the changed mod. +/// Parameter is the index of the changed group inside the mod. +/// Parameter is the index of the changed option inside the group or -1 if it does not concern a specific option. +/// Parameter is the index of the group an option was moved to. +/// +public sealed class ModOptionChanged : EventWrapper> +{ + public ModOptionChanged() + : base(nameof(ModOptionChanged)) + { } + + public void Invoke(ModOptionChangeType changeType, Mod mod, int groupIndex, int optionIndex, int moveToIndex) + => Invoke(this, changeType, mod, groupIndex, optionIndex, moveToIndex); +} + +/// Triggered whenever mods are prepared to be rediscovered. +public sealed class ModDiscoveryStarted : EventWrapper +{ + public ModDiscoveryStarted() + : base(nameof(ModDiscoveryStarted)) + { } + + public void Invoke() + => EventWrapper.Invoke(this); +} + +/// Triggered whenever a new mod discovery has finished. +public sealed class ModDiscoveryFinished : EventWrapper +{ + public ModDiscoveryFinished() + : base(nameof(ModDiscoveryFinished)) + { } + + public void Invoke() + => Invoke(this); +} + +/// +/// Triggered whenever the mod root directory changes. +/// +/// Parameter is the full path of the new directory. +/// Parameter is whether the new directory is valid. +/// +/// +public sealed class ModDirectoryChanged : EventWrapper> +{ + public ModDirectoryChanged() + : base(nameof(ModDirectoryChanged)) + { } + + public void Invoke(string newModDirectory, bool newDirectoryValid) + => Invoke(this, newModDirectory, newDirectoryValid); +} + +/// +/// Triggered whenever a mod is added, deleted, moved or reloaded. +/// +/// Parameter is the type of change. +/// Parameter is the changed mod. +/// Parameter is the old directory on deletion, move or reload and null on addition. +/// Parameter is the new directory on addition, move or reload and null on deletion. +/// +/// +public sealed class ModPathChanged : EventWrapper> +{ + public ModPathChanged() + : base(nameof(ModPathChanged)) + { } + + public void Invoke(ModPathChangeType changeType, Mod mod, DirectoryInfo? oldModDirectory, DirectoryInfo? newModDirectory) + => Invoke(this, changeType, mod, oldModDirectory, newModDirectory); +} + public class CommunicatorService : IDisposable { - /// - /// Triggered whenever collection setup is changed. - /// - /// Parameter is the type of the changed collection. (Inactive or Temporary for additions or deletions) - /// Parameter is the old collection, or null on additions. - /// Parameter is the new collection, or null on deletions. - /// Parameter is the display name for Individual collections or an empty string otherwise. - /// - public readonly EventWrapper CollectionChange = new(nameof(CollectionChange)); + /// + public readonly CollectionChange CollectionChange = new(); - /// - /// Triggered whenever a temporary mod for all collections is changed. - /// - /// Parameter added, deleted or edited temporary mod. - /// Parameter is whether the mod was newly created. - /// Parameter is whether the mod was deleted. - /// - public readonly EventWrapper TemporaryGlobalModChange = new(nameof(TemporaryGlobalModChange)); + /// + public readonly TemporaryGlobalModChange TemporaryGlobalModChange = new(); - /// - /// Triggered whenever a character base draw object is being created by the game. - /// - /// Parameter is the game object for which a draw object is created. - /// Parameter is the name of the applied collection. - /// Parameter is a pointer to the model id (an uint). - /// Parameter is a pointer to the customize array. - /// Parameter is a pointer to the equip data array. - /// - public readonly EventWrapper CreatingCharacterBase = new(nameof(CreatingCharacterBase)); + /// + public readonly CreatingCharacterBase CreatingCharacterBase = new(); - /// - /// Parameter is the game object for which a draw object is created. - /// Parameter is the name of the applied collection. - /// Parameter is the created draw object. - /// - public readonly EventWrapper CreatedCharacterBase = new(nameof(CreatedCharacterBase)); + /// + public readonly CreatedCharacterBase CreatedCharacterBase = new(); - /// - /// Triggered whenever mod meta data or local data is changed. - /// - /// Parameter is the type of data change for the mod, which can be multiple flags. - /// Parameter is the changed mod. - /// Parameter is the old name of the mod in case of a name change, and null otherwise. - /// - public readonly EventWrapper ModDataChanged = new(nameof(ModDataChanged)); + /// + public readonly ModDataChanged ModDataChanged = new(); - /// - /// Triggered whenever an option of a mod is changed inside the mod. - /// - /// Parameter is the type option change. - /// Parameter is the changed mod. - /// Parameter is the index of the changed group inside the mod. - /// Parameter is the index of the changed option inside the group or -1 if it does not concern a specific option. - /// Parameter is the index of the group an option was moved to. - /// - public readonly EventWrapper ModOptionChanged = new(nameof(ModOptionChanged)); + /// + public readonly ModOptionChanged ModOptionChanged = new(); + /// + public readonly ModDiscoveryStarted ModDiscoveryStarted = new(); - /// Triggered whenever mods are prepared to be rediscovered. - public readonly EventWrapper ModDiscoveryStarted = new(nameof(ModDiscoveryStarted)); + /// + public readonly ModDiscoveryFinished ModDiscoveryFinished = new(); - /// Triggered whenever a new mod discovery has finished. - public readonly EventWrapper ModDiscoveryFinished = new(nameof(ModDiscoveryFinished)); + /// + public readonly ModDirectoryChanged ModDirectoryChanged = new(); - /// - /// Triggered whenever the mod root directory changes. - /// - /// Parameter is the full path of the new directory. - /// Parameter is whether the new directory is valid. - /// - /// - public readonly EventWrapper ModDirectoryChanged = new(nameof(ModDirectoryChanged)); - - /// - /// Triggered whenever a mod is added, deleted, moved or reloaded. - /// - /// Parameter is the type of change. - /// Parameter is the changed mod. - /// Parameter is the old directory on deletion, move or reload and null on addition. - /// Parameter is the new directory on addition, move or reload and null on deletion. - /// - /// - public EventWrapper ModPathChanged = new(nameof(ModPathChanged)); + /// + public readonly ModPathChanged ModPathChanged = new(); public void Dispose() { diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index c2d64f92..422917a5 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -7,6 +7,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui.Filesystem; using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.Mods; using Penumbra.UI.Classes; using SixLabors.ImageSharp; @@ -25,8 +26,8 @@ public class ConfigMigrationService private Configuration _config = null!; private JObject _data = null!; - public string CurrentCollection = ModCollection.DefaultCollection; - public string DefaultCollection = ModCollection.DefaultCollection; + public string CurrentCollection = ModCollection.DefaultCollectionName; + public string DefaultCollection = ModCollection.DefaultCollectionName; public string ForcedCollection = string.Empty; public Dictionary CharacterCollections = new(); public Dictionary ModSortOrder = new(); @@ -87,7 +88,7 @@ public class ConfigMigrationService if (_config.Version != 6) return; - CollectionManager.MigrateUngenderedCollections(_fileNames); + ActiveCollectionMigration.MigrateUngenderedCollections(_fileNames); _config.Version = 7; } @@ -257,11 +258,11 @@ public class ConfigMigrationService using var j = new JsonTextWriter(writer); j.Formatting = Formatting.Indented; j.WriteStartObject(); - j.WritePropertyName(nameof(CollectionManager.Default)); + j.WritePropertyName(nameof(ActiveCollections.Default)); j.WriteValue(def); - j.WritePropertyName(nameof(CollectionManager.Interface)); + j.WritePropertyName(nameof(ActiveCollections.Interface)); j.WriteValue(ui); - j.WritePropertyName(nameof(CollectionManager.Current)); + j.WritePropertyName(nameof(ActiveCollections.Current)); j.WriteValue(current); foreach (var (type, collection) in special) { @@ -305,7 +306,7 @@ public class ConfigMigrationService if (!collectionJson.Exists) return; - var defaultCollection = ModCollection.CreateNewEmpty(ModCollection.DefaultCollection); + var defaultCollection = ModCollection.CreateNewEmpty(ModCollection.DefaultCollectionName); var defaultCollectionFile = new FileInfo(_fileNames.CollectionFile(defaultCollection)); if (defaultCollectionFile.Exists) return; @@ -338,7 +339,7 @@ public class ConfigMigrationService if (!InvertModListOrder) dict = dict.ToDictionary(kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority }); - defaultCollection = ModCollection.MigrateFromV0(ModCollection.DefaultCollection, dict); + defaultCollection = ModCollection.MigrateFromV0(ModCollection.DefaultCollectionName, dict); Penumbra.SaveService.ImmediateSave(defaultCollection); } catch (Exception e) diff --git a/Penumbra/Services/FilenameService.cs b/Penumbra/Services/FilenameService.cs index d7060e05..b5fa5487 100644 --- a/Penumbra/Services/FilenameService.cs +++ b/Penumbra/Services/FilenameService.cs @@ -27,13 +27,13 @@ public class FilenameService ActiveCollectionsFile = Path.Combine(pi.ConfigDirectory.FullName, "active_collections.json"); } - /// Obtain the path of a collection file given its name. Returns an empty string if the collection is temporary. + /// Obtain the path of a collection file given its name. public string CollectionFile(ModCollection collection) - => collection.Index >= 0 ? Path.Combine(CollectionDirectory, $"{collection.Name.RemoveInvalidPathSymbols()}.json") : string.Empty; + => CollectionFile(collection.Name); /// Obtain the path of a collection file given its name. public string CollectionFile(string collectionName) - => Path.Combine(CollectionDirectory, $"{collectionName.RemoveInvalidPathSymbols()}.json"); + => Path.Combine(CollectionDirectory, $"{collectionName}.json"); /// Obtain the path of the local data file given a mod directory. Returns an empty string if the mod is temporary. diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 17c58dc6..cc5e7cb6 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -12,6 +12,7 @@ using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.Mods; @@ -54,9 +55,9 @@ public class ItemSwapTab : IDisposable, ITab // @formatter:on }; - _communicator.CollectionChange.Event += OnCollectionChange; - _collectionManager.Current.ModSettingChanged += OnSettingChange; - _communicator.ModOptionChanged.Event += OnModOptionChange; + _communicator.CollectionChange.Subscribe(OnCollectionChange); + _collectionManager.Active.Current.ModSettingChanged += OnSettingChange; + _communicator.ModOptionChanged.Subscribe(OnModOptionChange); } /// Update the currently selected mod or its settings. @@ -99,9 +100,9 @@ public class ItemSwapTab : IDisposable, ITab public void Dispose() { - _communicator.CollectionChange.Event -= OnCollectionChange; - _collectionManager.Current.ModSettingChanged -= OnSettingChange; - _communicator.ModOptionChanged.Event -= OnModOptionChange; + _communicator.CollectionChange.Unsubscribe(OnCollectionChange); + _collectionManager.Active.Current.ModSettingChanged -= OnSettingChange; + _communicator.ModOptionChanged.Unsubscribe(OnModOptionChange); } private enum SwapType @@ -199,7 +200,7 @@ public class ItemSwapTab : IDisposable, ITab var values = _selectors[_lastTab]; if (values.Source.CurrentSelection.Item2 != null && values.Target.CurrentSelection.Item2 != null) _affectedItems = _swapData.LoadEquipment(values.Target.CurrentSelection.Item2, values.Source.CurrentSelection.Item2, - _useCurrentCollection ? _collectionManager.Current : null, _useRightRing, _useLeftRing); + _useCurrentCollection ? _collectionManager.Active.Current : null, _useRightRing, _useLeftRing); break; case SwapType.BetweenSlots: @@ -208,27 +209,27 @@ public class ItemSwapTab : IDisposable, ITab if (selectorFrom.CurrentSelection.Item2 != null && selectorTo.CurrentSelection.Item2 != null) _affectedItems = _swapData.LoadTypeSwap(_slotTo, selectorTo.CurrentSelection.Item2, _slotFrom, selectorFrom.CurrentSelection.Item2, - _useCurrentCollection ? _collectionManager.Current : null); + _useCurrentCollection ? _collectionManager.Active.Current : null); break; case SwapType.Hair when _targetId > 0 && _sourceId > 0: _swapData.LoadCustomization(BodySlot.Hair, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId, (SetId)_targetId, - _useCurrentCollection ? _collectionManager.Current : null); + _useCurrentCollection ? _collectionManager.Active.Current : null); break; case SwapType.Face when _targetId > 0 && _sourceId > 0: _swapData.LoadCustomization(BodySlot.Face, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId, (SetId)_targetId, - _useCurrentCollection ? _collectionManager.Current : null); + _useCurrentCollection ? _collectionManager.Active.Current : null); break; case SwapType.Ears when _targetId > 0 && _sourceId > 0: _swapData.LoadCustomization(BodySlot.Zear, Names.CombinedRace(_currentGender, ModelRace.Viera), (SetId)_sourceId, (SetId)_targetId, - _useCurrentCollection ? _collectionManager.Current : null); + _useCurrentCollection ? _collectionManager.Active.Current : null); break; case SwapType.Tail when _targetId > 0 && _sourceId > 0: _swapData.LoadCustomization(BodySlot.Tail, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId, (SetId)_targetId, - _useCurrentCollection ? _collectionManager.Current : null); + _useCurrentCollection ? _collectionManager.Active.Current : null); break; case SwapType.Weapon: break; } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index d4980936..59a78306 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -52,7 +52,7 @@ public partial class ModEditWindow : Window, IDisposable _modelTab.Reset(); _materialTab.Reset(); _shaderPackageTab.Reset(); - _itemSwapTab.UpdateMod(mod, Penumbra.CollectionManager.Current[mod.Index].Settings); + _itemSwapTab.UpdateMod(mod, Penumbra.CollectionManager.Active.Current[mod.Index].Settings); } public void ChangeOption(SubMod? subMod) @@ -475,7 +475,7 @@ public partial class ModEditWindow : Window, IDisposable /// private FullPath FindBestMatch(Utf8GamePath path) { - var currentFile = Penumbra.CollectionManager.Current.ResolvePath(path); + var currentFile = Penumbra.CollectionManager.Active.Current.ResolvePath(path); if (currentFile != null) return currentFile.Value; diff --git a/Penumbra/UI/CollectionTab/Collections.CollectionSelector.cs b/Penumbra/UI/CollectionTab/Collections.CollectionSelector.cs index 1ff60559..41aa1437 100644 --- a/Penumbra/UI/CollectionTab/Collections.CollectionSelector.cs +++ b/Penumbra/UI/CollectionTab/Collections.CollectionSelector.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using ImGuiNET; using OtterGui.Widgets; using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; namespace Penumbra.UI.CollectionTab; @@ -17,16 +18,16 @@ public sealed class CollectionSelector : FilterComboCache public void Draw(string label, float width, int individualIdx) { - var (_, collection) = _collectionManager.Individuals[individualIdx]; + var (_, collection) = _collectionManager.Active.Individuals[individualIdx]; if (Draw(label, collection.Name, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()) && CurrentSelection != null) - _collectionManager.SetCollection(CurrentSelection, CollectionType.Individual, individualIdx); + _collectionManager.Active.SetCollection(CurrentSelection, CollectionType.Individual, individualIdx); } public void Draw(string label, float width, CollectionType type) { - var current = _collectionManager.ByType(type, ActorIdentifier.Invalid); + var current = _collectionManager.Active.ByType(type, ActorIdentifier.Invalid); if (Draw(label, current?.Name ?? string.Empty, string.Empty, width, ImGui.GetTextLineHeightWithSpacing()) && CurrentSelection != null) - _collectionManager.SetCollection(CurrentSelection, type); + _collectionManager.Active.SetCollection(CurrentSelection, type); } protected override string ToString(ModCollection obj) diff --git a/Penumbra/UI/CollectionTab/Collections.IndividualCollectionUi.cs b/Penumbra/UI/CollectionTab/Collections.IndividualCollectionUi.cs index b7bcc0f5..7867f2b3 100644 --- a/Penumbra/UI/CollectionTab/Collections.IndividualCollectionUi.cs +++ b/Penumbra/UI/CollectionTab/Collections.IndividualCollectionUi.cs @@ -8,6 +8,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.Services; @@ -45,7 +46,7 @@ public class IndividualCollectionUi + $"More general {TutorialService.GroupAssignment} or the {TutorialService.DefaultCollection} do not apply if an Individual Collection takes effect.\n" + "Certain related actors - like the ones in cutscenes or preview windows - will try to use appropriate individual collections."); ImGui.Separator(); - for (var i = 0; i < _collectionManager.Individuals.Count; ++i) + for (var i = 0; i < _collectionManager.Active.Individuals.Count; ++i) { DrawIndividualAssignment(i); } @@ -138,13 +139,13 @@ public class IndividualCollectionUi /// Draw a single individual assignment. private void DrawIndividualAssignment(int idx) { - var (name, _) = _collectionManager.Individuals[idx]; + var (name, _) = _collectionManager.Active.Individuals[idx]; using var id = ImRaii.PushId(idx); _withEmpty.Draw("##IndividualCombo", UiHelpers.InputTextWidth.X, idx); ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, string.Empty, false, true)) - _collectionManager.RemoveIndividualCollection(idx); + _collectionManager.Active.RemoveIndividualCollection(idx); ImGui.SameLine(); ImGui.AlignTextToFramePadding(); @@ -163,7 +164,7 @@ public class IndividualCollectionUi return; if (_individualDragDropIdx >= 0) - _collectionManager.MoveIndividualCollection(_individualDragDropIdx, idx); + _collectionManager.Active.MoveIndividualCollection(_individualDragDropIdx, idx); _individualDragDropIdx = -1; } @@ -178,7 +179,7 @@ public class IndividualCollectionUi if (ImGuiUtil.DrawDisabledButton("Assign Player", buttonWidth, _newPlayerTooltip, _newPlayerTooltip.Length > 0 || _newPlayerIdentifiers.Length == 0)) { - _collectionManager.CreateIndividualCollection(_newPlayerIdentifiers); + _collectionManager.Active.CreateIndividualCollection(_newPlayerIdentifiers); change = true; } @@ -196,7 +197,7 @@ public class IndividualCollectionUi if (ImGuiUtil.DrawDisabledButton("Assign NPC", buttonWidth, _newNpcTooltip, _newNpcIdentifiers.Length == 0 || _newNpcTooltip.Length > 0)) { - _collectionManager.CreateIndividualCollection(_newNpcIdentifiers); + _collectionManager.Active.CreateIndividualCollection(_newNpcIdentifiers); change = true; } @@ -209,7 +210,7 @@ public class IndividualCollectionUi _newOwnedIdentifiers.Length == 0 || _newOwnedTooltip.Length > 0)) return false; - _collectionManager.CreateIndividualCollection(_newOwnedIdentifiers); + _collectionManager.Active.CreateIndividualCollection(_newOwnedIdentifiers); return true; } @@ -220,7 +221,7 @@ public class IndividualCollectionUi _newRetainerIdentifiers.Length == 0 || _newRetainerTooltip.Length > 0)) return false; - _collectionManager.CreateIndividualCollection(_newRetainerIdentifiers); + _collectionManager.Active.CreateIndividualCollection(_newRetainerIdentifiers); return true; } @@ -264,7 +265,7 @@ public class IndividualCollectionUi private bool DrawNewCurrentPlayerCollection(Vector2 width) { var player = _actorService.AwaitedService.GetCurrentPlayer(); - var result = _collectionManager.Individuals.CanAdd(player); + var result = _collectionManager.Active.Individuals.CanAdd(player); var tt = result switch { IndividualCollections.AddResult.Valid => $"Assign a collection to {player}.", @@ -277,7 +278,7 @@ public class IndividualCollectionUi if (!ImGuiUtil.DrawDisabledButton("Assign Current Player", width, tt, result != IndividualCollections.AddResult.Valid)) return false; - _collectionManager.CreateIndividualCollection(player); + _collectionManager.Active.CreateIndividualCollection(player); return true; } @@ -285,7 +286,7 @@ public class IndividualCollectionUi private bool DrawNewTargetCollection(Vector2 width) { var target = _actorService.AwaitedService.FromObject(DalamudServices.Targets.Target, false, true, true); - var result = _collectionManager.Individuals.CanAdd(target); + var result = _collectionManager.Active.Individuals.CanAdd(target); var tt = result switch { IndividualCollections.AddResult.Valid => $"Assign a collection to {target}.", @@ -295,7 +296,7 @@ public class IndividualCollectionUi }; if (ImGuiUtil.DrawDisabledButton("Assign Current Target", width, tt, result != IndividualCollections.AddResult.Valid)) { - _collectionManager.CreateIndividualCollection(_collectionManager.Individuals.GetGroup(target)); + _collectionManager.Active.CreateIndividualCollection(_collectionManager.Active.Individuals.GetGroup(target)); return true; } @@ -311,7 +312,7 @@ public class IndividualCollectionUi private void UpdateIdentifiers() { var combo = GetNpcCombo(_newKind); - _newPlayerTooltip = _collectionManager.Individuals.CanAdd(IdentifierType.Player, _newCharacterName, + _newPlayerTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Player, _newCharacterName, _worldCombo.CurrentSelection.Key, ObjectKind.None, Array.Empty(), out _newPlayerIdentifiers) switch { @@ -320,7 +321,7 @@ public class IndividualCollectionUi IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, _ => string.Empty, }; - _newRetainerTooltip = _collectionManager.Individuals.CanAdd(IdentifierType.Retainer, _newCharacterName, 0, ObjectKind.None, + _newRetainerTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Retainer, _newCharacterName, 0, ObjectKind.None, Array.Empty(), out _newRetainerIdentifiers) switch { _ when _newCharacterName.Length == 0 => NewRetainerTooltipEmpty, @@ -330,13 +331,13 @@ public class IndividualCollectionUi }; if (combo.CurrentSelection.Ids != null) { - _newNpcTooltip = _collectionManager.Individuals.CanAdd(IdentifierType.Npc, string.Empty, ushort.MaxValue, _newKind, + _newNpcTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Npc, string.Empty, ushort.MaxValue, _newKind, combo.CurrentSelection.Ids, out _newNpcIdentifiers) switch { IndividualCollections.AddResult.AlreadySet => AlreadyAssigned, _ => string.Empty, }; - _newOwnedTooltip = _collectionManager.Individuals.CanAdd(IdentifierType.Owned, _newCharacterName, + _newOwnedTooltip = _collectionManager.Active.Individuals.CanAdd(IdentifierType.Owned, _newCharacterName, _worldCombo.CurrentSelection.Key, _newKind, combo.CurrentSelection.Ids, out _newOwnedIdentifiers) switch { diff --git a/Penumbra/UI/CollectionTab/Collections.InheritanceUi.cs b/Penumbra/UI/CollectionTab/Collections.InheritanceUi.cs index 57a51ab1..31044cae 100644 --- a/Penumbra/UI/CollectionTab/Collections.InheritanceUi.cs +++ b/Penumbra/UI/CollectionTab/Collections.InheritanceUi.cs @@ -7,6 +7,7 @@ using ImGuiNET; using OtterGui; using OtterGui.Raii; using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.UI.Classes; namespace Penumbra.UI.CollectionTab; @@ -116,8 +117,8 @@ public class InheritanceUi return; _seenInheritedCollections.Clear(); - _seenInheritedCollections.Add(_collectionManager.Current); - foreach (var collection in _collectionManager.Current.Inheritance.ToList()) + _seenInheritedCollections.Add(_collectionManager.Active.Current); + foreach (var collection in _collectionManager.Active.Current.Inheritance.ToList()) DrawInheritance(collection); } @@ -135,7 +136,7 @@ public class InheritanceUi using var target = ImRaii.DragDropTarget(); if (target.Success && ImGuiUtil.IsDropping(InheritanceDragDropLabel)) - _inheritanceAction = (_collectionManager.Current.Inheritance.IndexOf(_movedInheritance!), -1); + _inheritanceAction = (_collectionManager.Active.Current.Inheritance.IndexOf(_movedInheritance!), -1); } /// @@ -146,7 +147,7 @@ public class InheritanceUi { if (_newCurrentCollection != null) { - _collectionManager.SetCollection(_newCurrentCollection, CollectionType.Current); + _collectionManager.Active.SetCollection(_newCurrentCollection, CollectionType.Current); _newCurrentCollection = null; } @@ -156,9 +157,9 @@ public class InheritanceUi if (_inheritanceAction.Value.Item1 >= 0) { if (_inheritanceAction.Value.Item2 == -1) - _collectionManager.Current.RemoveInheritance(_inheritanceAction.Value.Item1); + _collectionManager.Active.Current.RemoveInheritance(_inheritanceAction.Value.Item1); else - _collectionManager.Current.MoveInheritance(_inheritanceAction.Value.Item1, _inheritanceAction.Value.Item2); + _collectionManager.Active.Current.MoveInheritance(_inheritanceAction.Value.Item1, _inheritanceAction.Value.Item2); } _inheritanceAction = null; @@ -172,7 +173,7 @@ public class InheritanceUi { DrawNewInheritanceCombo(); ImGui.SameLine(); - var inheritance = _collectionManager.Current.CheckValidInheritance(_newInheritance); + var inheritance = _collectionManager.Active.Current.CheckValidInheritance(_newInheritance); var tt = inheritance switch { ModCollection.ValidInheritance.Empty => "No valid collection to inherit from selected.", @@ -184,7 +185,7 @@ public class InheritanceUi }; if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, tt, inheritance != ModCollection.ValidInheritance.Valid, true) - && _collectionManager.Current.AddInheritance(_newInheritance!, true)) + && _collectionManager.Active.Current.AddInheritance(_newInheritance!, true)) _newInheritance = null; if (inheritance != ModCollection.ValidInheritance.Valid) @@ -232,15 +233,15 @@ public class InheritanceUi private void DrawNewInheritanceCombo() { ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton); - _newInheritance ??= _collectionManager.FirstOrDefault(c - => c != _collectionManager.Current && !_collectionManager.Current.Inheritance.Contains(c)) + _newInheritance ??= _collectionManager.Storage.FirstOrDefault(c + => c != _collectionManager.Active.Current && !_collectionManager.Active.Current.Inheritance.Contains(c)) ?? ModCollection.Empty; using var combo = ImRaii.Combo("##newInheritance", _newInheritance.Name); if (!combo) return; - foreach (var collection in _collectionManager - .Where(c => _collectionManager.Current.CheckValidInheritance(c) == ModCollection.ValidInheritance.Valid) + foreach (var collection in _collectionManager.Storage + .Where(c => _collectionManager.Active.Current.CheckValidInheritance(c) == ModCollection.ValidInheritance.Valid) .OrderBy(c => c.Name)) { if (ImGui.Selectable(collection.Name, _newInheritance == collection)) @@ -260,8 +261,8 @@ public class InheritanceUi if (_movedInheritance != null) { - var idx1 = _collectionManager.Current.Inheritance.IndexOf(_movedInheritance); - var idx2 = _collectionManager.Current.Inheritance.IndexOf(collection); + var idx1 = _collectionManager.Active.Current.Inheritance.IndexOf(_movedInheritance); + var idx2 = _collectionManager.Active.Current.Inheritance.IndexOf(collection); if (idx1 >= 0 && idx2 >= 0) _inheritanceAction = (idx1, idx2); } @@ -291,7 +292,7 @@ public class InheritanceUi if (ImGui.GetIO().KeyCtrl && ImGui.IsItemClicked(ImGuiMouseButton.Right)) { if (withDelete && ImGui.GetIO().KeyShift) - _inheritanceAction = (_collectionManager.Current.Inheritance.IndexOf(collection), -1); + _inheritanceAction = (_collectionManager.Active.Current.Inheritance.IndexOf(collection), -1); else _newCurrentCollection = collection; } diff --git a/Penumbra/UI/CollectionTab/Collections.SpecialCombo.cs b/Penumbra/UI/CollectionTab/Collections.SpecialCombo.cs index 5af7c578..91b7f491 100644 --- a/Penumbra/UI/CollectionTab/Collections.SpecialCombo.cs +++ b/Penumbra/UI/CollectionTab/Collections.SpecialCombo.cs @@ -2,6 +2,7 @@ using ImGuiNET; using OtterGui.Classes; using OtterGui.Widgets; using Penumbra.Collections; +using Penumbra.Collections.Manager; namespace Penumbra.UI.CollectionTab; @@ -37,6 +38,6 @@ public sealed class SpecialCombo : FilterComboBase<(CollectionType, string, stri protected override bool IsVisible(int globalIdx, LowerString filter) { var obj = Items[globalIdx]; - return filter.IsContained(obj.Item2) && _collectionManager.ByType(obj.Item1) == null; + return filter.IsContained(obj.Item2) && _collectionManager.Active.ByType(obj.Item1) == null; } } \ No newline at end of file diff --git a/Penumbra/UI/FileDialogService.cs b/Penumbra/UI/FileDialogService.cs index c6a7d451..b1956796 100644 --- a/Penumbra/UI/FileDialogService.cs +++ b/Penumbra/UI/FileDialogService.cs @@ -24,7 +24,7 @@ public class FileDialogService : IDisposable { _communicator = communicator; _manager = SetupFileManager(config.ModDirectory); - _communicator.ModDirectoryChanged.Event += OnModDirectoryChange; + _communicator.ModDirectoryChanged.Subscribe(OnModDirectoryChange); } public void OpenFilePicker(string title, string filters, Action> callback, int selectionCountMax, string? startPath, @@ -71,7 +71,7 @@ public class FileDialogService : IDisposable { _startPaths.Clear(); _manager.Reset(); - _communicator.ModDirectoryChanged.Event -= OnModDirectoryChange; + _communicator.ModDirectoryChanged.Unsubscribe(OnModDirectoryChange); } private string? GetStartPath(string title, string? startPath, bool forceStartPath) diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 72e9a362..1d072d89 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -14,10 +14,11 @@ using OtterGui.FileSystem.Selector; using OtterGui.Raii; using Penumbra.Api.Enums; using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.Import; using Penumbra.Import.Structs; using Penumbra.Mods; -using Penumbra.Mods.Manager; +using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.Classes; using Penumbra.Util; @@ -79,25 +80,25 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector @@ -495,7 +496,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector !c.Solved) ? ColorId.ConflictingMod @@ -657,7 +658,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector 0) { if (conflicts.Any(c => !c.Solved)) @@ -727,7 +728,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector "Conflicts"u8; public bool IsVisible - => _collectionManager.Current.Conflicts(_selector.Selected!).Count > 0; + => _collectionManager.Active.Current.Conflicts(_selector.Selected!).Count > 0; public void DrawContent() { @@ -36,7 +36,7 @@ public class ModPanelConflictsTab : ITab // Can not be null because otherwise the tab bar is never drawn. var mod = _selector.Selected!; - foreach (var conflict in Penumbra.CollectionManager.Current.Conflicts(mod)) + foreach (var conflict in _collectionManager.Active.Current.Conflicts(mod)) { if (ImGui.Selectable(conflict.Mod2.Name) && conflict.Mod2 is Mod otherMod) _selector.SelectByValue(otherMod); @@ -47,7 +47,7 @@ public class ModPanelConflictsTab : ITab { var priority = conflict.Mod2.Index < 0 ? conflict.Mod2.Priority - : _collectionManager.Current[conflict.Mod2.Index].Settings!.Priority; + : _collectionManager.Active.Current[conflict.Mod2.Index].Settings!.Priority; ImGui.TextUnformatted($"(Priority {priority})"); } diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index ffae30d2..f4407841 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -12,6 +12,7 @@ using Penumbra.Mods; using Penumbra.UI.Classes; using Dalamud.Interface.Components; using Dalamud.Interface; +using Penumbra.Collections.Manager; using Penumbra.Mods.Manager; namespace Penumbra.UI.ModsTab; @@ -59,7 +60,7 @@ public class ModPanelSettingsTab : ITab _settings = _selector.SelectedSettings; _collection = _selector.SelectedSettingCollection; - _inherited = _collection != _collectionManager.Current; + _inherited = _collection != _collectionManager.Active.Current; _empty = _settings == ModSettings.Empty; DrawInheritedWarning(); @@ -113,7 +114,7 @@ public class ModPanelSettingsTab : ITab using var color = ImRaii.PushColor(ImGuiCol.Button, Colors.PressEnterWarningBg); var width = new Vector2(ImGui.GetContentRegionAvail().X, 0); if (ImGui.Button($"These settings are inherited from {_collection.Name}.", width)) - _collectionManager.Current.SetModInheritance(_selector.Selected!.Index, false); + _collectionManager.Active.Current.SetModInheritance(_selector.Selected!.Index, false); ImGuiUtil.HoverTooltip("You can click this button to copy the current settings to the current selection.\n" + "You can also just change any setting, which will copy the settings with the single setting changed to the current selection."); @@ -127,7 +128,7 @@ public class ModPanelSettingsTab : ITab return; _modManager.SetKnown(_selector.Selected!); - _collectionManager.Current.SetModState(_selector.Selected!.Index, enabled); + _collectionManager.Active.Current.SetModState(_selector.Selected!.Index, enabled); } /// @@ -145,7 +146,7 @@ public class ModPanelSettingsTab : ITab if (ImGui.IsItemDeactivatedAfterEdit() && _currentPriority.HasValue) { if (_currentPriority != _settings.Priority) - _collectionManager.Current.SetModPriority(_selector.Selected!.Index, _currentPriority.Value); + _collectionManager.Active.Current.SetModPriority(_selector.Selected!.Index, _currentPriority.Value); _currentPriority = null; } @@ -167,7 +168,7 @@ public class ModPanelSettingsTab : ITab var scroll = ImGui.GetScrollMaxY() > 0 ? ImGui.GetStyle().ScrollbarSize : 0; ImGui.SameLine(ImGui.GetWindowWidth() - ImGui.CalcTextSize(text).X - ImGui.GetStyle().FramePadding.X * 2 - scroll); if (ImGui.Button(text)) - _collectionManager.Current.SetModInheritance(_selector.Selected!.Index, true); + _collectionManager.Active.Current.SetModInheritance(_selector.Selected!.Index, true); ImGuiUtil.HoverTooltip("Remove current settings from this collection so that it can inherit them.\n" + "If no inherited collection has settings for this mod, it will be disabled."); @@ -190,7 +191,7 @@ public class ModPanelSettingsTab : ITab id.Push(idx2); var option = group[idx2]; if (ImGui.Selectable(option.Name, idx2 == selectedOption)) - _collectionManager.Current.SetModSetting(_selector.Selected!.Index, groupIdx, (uint)idx2); + _collectionManager.Active.Current.SetModSetting(_selector.Selected!.Index, groupIdx, (uint)idx2); if (option.Description.Length > 0) { @@ -235,7 +236,7 @@ public class ModPanelSettingsTab : ITab using var i = ImRaii.PushId(idx); var option = group[idx]; if (ImGui.RadioButton(option.Name, selectedOption == idx)) - _collectionManager.Current.SetModSetting(_selector.Selected!.Index, groupIdx, (uint)idx); + _collectionManager.Active.Current.SetModSetting(_selector.Selected!.Index, groupIdx, (uint)idx); if (option.Description.Length <= 0) continue; @@ -320,7 +321,7 @@ public class ModPanelSettingsTab : ITab if (ImGui.Checkbox(option.Name, ref setting)) { flags = setting ? flags | flag : flags & ~flag; - _collectionManager.Current.SetModSetting(_selector.Selected!.Index, groupIdx, flags); + _collectionManager.Active.Current.SetModSetting(_selector.Selected!.Index, groupIdx, flags); } if (option.Description.Length > 0) @@ -348,10 +349,10 @@ public class ModPanelSettingsTab : ITab if (ImGui.Selectable("Enable All")) { flags = group.Count == 32 ? uint.MaxValue : (1u << group.Count) - 1u; - _collectionManager.Current.SetModSetting(_selector.Selected!.Index, groupIdx, flags); + _collectionManager.Active.Current.SetModSetting(_selector.Selected!.Index, groupIdx, flags); } if (ImGui.Selectable("Disable All")) - _collectionManager.Current.SetModSetting(_selector.Selected!.Index, groupIdx, 0); + _collectionManager.Active.Current.SetModSetting(_selector.Selected!.Index, groupIdx, 0); } } diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs index e5f9083e..7f517634 100644 --- a/Penumbra/UI/Tabs/ChangedItemsTab.cs +++ b/Penumbra/UI/Tabs/ChangedItemsTab.cs @@ -8,7 +8,7 @@ using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Api; -using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.Mods; using Penumbra.UI.Classes; @@ -17,7 +17,7 @@ namespace Penumbra.UI.Tabs; public class ChangedItemsTab : ITab { private readonly CollectionManager _collectionManager; - private readonly PenumbraApi _api; + private readonly PenumbraApi _api; public ChangedItemsTab(CollectionManager collectionManager, PenumbraApi api) { @@ -49,7 +49,7 @@ public class ChangedItemsTab : ITab ImGui.TableSetupColumn("mods", flags, varWidth - 120 * UiHelpers.Scale); ImGui.TableSetupColumn("id", flags, 120 * UiHelpers.Scale); - var items = _collectionManager.Current.ChangedItems; + var items = _collectionManager.Active.Current.ChangedItems; var rest = _changedItemFilter.IsEmpty && _changedItemModFilter.IsEmpty ? ImGuiClip.ClippedDraw(items, skips, DrawChangedItemColumn, items.Count) : ImGuiClip.FilteredClippedDraw(items, skips, FilterChangedItem, DrawChangedItemColumn); diff --git a/Penumbra/UI/Tabs/CollectionsTab.cs b/Penumbra/UI/Tabs/CollectionsTab.cs index afd739a8..18dd3b95 100644 --- a/Penumbra/UI/Tabs/CollectionsTab.cs +++ b/Penumbra/UI/Tabs/CollectionsTab.cs @@ -8,6 +8,7 @@ using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.Services; using Penumbra.UI.CollectionTab; @@ -35,12 +36,12 @@ public class CollectionsTab : IDisposable, ITab _config = config; _specialCollectionCombo = new SpecialCombo(_collectionManager, "##NewSpecial", 350); _collectionsWithEmpty = new CollectionSelector(_collectionManager, - () => _collectionManager.OrderBy(c => c.Name).Prepend(ModCollection.Empty).ToList()); - _collectionSelector = new CollectionSelector(_collectionManager, () => _collectionManager.OrderBy(c => c.Name).ToList()); + () => _collectionManager.Storage.OrderBy(c => c.Name).Prepend(ModCollection.Empty).ToList()); + _collectionSelector = new CollectionSelector(_collectionManager, () => _collectionManager.Storage.OrderBy(c => c.Name).ToList()); _inheritance = new InheritanceUi(_collectionManager); _individualCollections = new IndividualCollectionUi(actorService, _collectionManager, _collectionsWithEmpty); - _communicator.CollectionChange.Event += _individualCollections.UpdateIdentifiers; + _communicator.CollectionChange.Subscribe(_individualCollections.UpdateIdentifiers); } public ReadOnlySpan Label @@ -51,7 +52,7 @@ public class CollectionsTab : IDisposable, ITab => (withEmpty ? _collectionsWithEmpty : _collectionSelector).Draw(label, width, collectionType); public void Dispose() - => _communicator.CollectionChange.Event -= _individualCollections.UpdateIdentifiers; + => _communicator.CollectionChange.Unsubscribe(_individualCollections.UpdateIdentifiers); /// Draw a tutorial step regardless of tab selection. public void DrawHeader() @@ -79,22 +80,22 @@ public class CollectionsTab : IDisposable, ITab /// private void CreateNewCollection(bool duplicate) { - if (_collectionManager.AddCollection(_newCollectionName, duplicate ? _collectionManager.Current : null)) + if (_collectionManager.Storage.AddCollection(_newCollectionName, duplicate ? _collectionManager.Active.Current : null)) _newCollectionName = string.Empty; } /// Draw the Clean Unused Settings button if there are any. private void DrawCleanCollectionButton(Vector2 width) { - if (!_collectionManager.Current.HasUnusedSettings) + if (!_collectionManager.Active.Current.HasUnusedSettings) return; ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton( - $"Clean {_collectionManager.Current.NumUnusedSettings} Unused Settings###CleanSettings", width + $"Clean {_collectionManager.Active.Current.NumUnusedSettings} Unused Settings###CleanSettings", width , "Remove all stored settings for mods not currently available and fix invalid settings.\n\nUse at own risk." , false)) - _collectionManager.Current.CleanUnavailableSettings(); + _collectionManager.Active.Current.CleanUnavailableSettings(); } /// Draw the new collection input as well as its buttons. @@ -103,7 +104,7 @@ public class CollectionsTab : IDisposable, ITab // Input for new collection name. Also checks for validity when changed. ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); if (ImGui.InputTextWithHint("##New Collection", "New Collection Name...", ref _newCollectionName, 64)) - _canAddCollection = _collectionManager.CanAddCollection(_newCollectionName, out _); + _canAddCollection = _collectionManager.Storage.CanAddCollection(_newCollectionName, out _); ImGui.SameLine(); ImGuiComponents.HelpMarker( @@ -161,14 +162,14 @@ public class CollectionsTab : IDisposable, ITab "This collection will be modified when using the Installed Mods tab and making changes.\nIt is not automatically assigned to anything."); // Deletion conditions. - var deleteCondition = _collectionManager.Current.Name != ModCollection.DefaultCollection; + var deleteCondition = _collectionManager.Active.Current.Name != ModCollection.DefaultCollectionName; var modifierHeld = Penumbra.Config.DeleteModModifier.IsActive(); var tt = deleteCondition ? modifierHeld ? string.Empty : $"Hold {_config.DeleteModModifier} while clicking to delete the collection." - : $"You can not delete the collection {ModCollection.DefaultCollection}."; + : $"You can not delete the collection {ModCollection.DefaultCollectionName}."; if (ImGuiUtil.DrawDisabledButton($"Delete {TutorialService.SelectedCollection}", width, tt, !deleteCondition || !modifierHeld)) - _collectionManager.RemoveCollection(_collectionManager.Current); + _collectionManager.Storage.RemoveCollection(_collectionManager.Active.Current); DrawCleanCollectionButton(width); } @@ -218,11 +219,11 @@ public class CollectionsTab : IDisposable, ITab { ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X); if (_specialCollectionCombo.CurrentIdx == -1 - || _collectionManager.ByType(_specialCollectionCombo.CurrentType!.Value.Item1) != null) + || _collectionManager.Active.ByType(_specialCollectionCombo.CurrentType!.Value.Item1) != null) { _specialCollectionCombo.ResetFilter(); _specialCollectionCombo.CurrentIdx = CollectionTypeExtensions.Special - .IndexOf(t => _collectionManager.ByType(t.Item1) == null); + .IndexOf(t => _collectionManager.Active.ByType(t.Item1) == null); } if (_specialCollectionCombo.CurrentType == null) @@ -238,7 +239,7 @@ public class CollectionsTab : IDisposable, ITab if (!ImGuiUtil.DrawDisabledButton($"Assign {TutorialService.ConditionalGroup}", new Vector2(120 * UiHelpers.Scale, 0), tt, disabled)) return; - _collectionManager.CreateSpecialCollection(_specialCollectionCombo.CurrentType!.Value.Item1); + _collectionManager.Active.CreateSpecialCollection(_specialCollectionCombo.CurrentType!.Value.Item1); _specialCollectionCombo.CurrentIdx = -1; } @@ -274,7 +275,7 @@ public class CollectionsTab : IDisposable, ITab { foreach (var (type, name, desc) in CollectionTypeExtensions.Special) { - var collection = _collectionManager.ByType(type); + var collection = _collectionManager.Active.ByType(type); if (collection == null) continue; @@ -284,7 +285,7 @@ public class CollectionsTab : IDisposable, ITab if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, string.Empty, false, true)) { - _collectionManager.RemoveSpecialCollection(type); + _collectionManager.Active.RemoveSpecialCollection(type); _specialCollectionCombo.ResetFilter(); } diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index f9259861..64fece1c 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -11,13 +11,12 @@ using ImGuiNET; using OtterGui; using OtterGui.Widgets; using Penumbra.Api; -using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.GameData.Files; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.PathResolving; using Penumbra.Interop.Structs; -using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.String; @@ -35,8 +34,8 @@ public class DebugTab : ITab private readonly StartTracker _timer; private readonly PerformanceTracker _performance; private readonly Configuration _config; - private readonly CollectionManager _collectionManager; - private readonly ModManager _modManager; + private readonly CollectionManager _collectionManager; + private readonly ModManager _modManager; private readonly ValidityChecker _validityChecker; private readonly HttpApi _httpApi; private readonly ActorService _actorService; @@ -136,10 +135,10 @@ public class DebugTab : ITab PrintValue("Penumbra Version", $"{_validityChecker.Version} {DebugVersionString}"); PrintValue("Git Commit Hash", _validityChecker.CommitHash); - PrintValue(TutorialService.SelectedCollection, _collectionManager.Current.Name); - PrintValue(" has Cache", _collectionManager.Current.HasCache.ToString()); - PrintValue(TutorialService.DefaultCollection, _collectionManager.Default.Name); - PrintValue(" has Cache", _collectionManager.Default.HasCache.ToString()); + PrintValue(TutorialService.SelectedCollection, _collectionManager.Active.Current.Name); + PrintValue(" has Cache", _collectionManager.Active.Current.HasCache.ToString()); + PrintValue(TutorialService.DefaultCollection, _collectionManager.Active.Default.Name); + PrintValue(" has Cache", _collectionManager.Active.Default.HasCache.ToString()); PrintValue("Mod Manager BasePath", _modManager.BasePath.Name); PrintValue("Mod Manager BasePath-Full", _modManager.BasePath.FullName); PrintValue("Mod Manager BasePath IsRooted", Path.IsPathRooted(_config.ModDirectory).ToString()); @@ -221,7 +220,7 @@ public class DebugTab : ITab using var table = Table("###DrawObjectResolverTable", 6, ImGuiTableFlags.SizingFixedFit); if (table) foreach (var (drawObject, (gameObjectPtr, child)) in _drawObjectState - .OrderBy(kvp => ((GameObject*)kvp.Value.Item1)->ObjectIndex) + .OrderBy(kvp => ((GameObject*)kvp.Value.Item1)->ObjectIndex) .ThenBy(kvp => kvp.Value.Item2) .ThenBy(kvp => kvp.Key)) { @@ -299,7 +298,7 @@ public class DebugTab : ITab { using var table = Table("##PathCollectionsIdentifiedTable", 4, ImGuiTableFlags.SizingFixedFit); if (table) - foreach (var (address, identifier, collection) in _identifiedCollectionCache + foreach (var (address, identifier, collection) in _identifiedCollectionCache .OrderBy(kvp => ((GameObject*)kvp.Address)->ObjectIndex)) { ImGuiUtil.DrawTableColumn($"{((GameObject*)address)->ObjectIndex}"); diff --git a/Penumbra/UI/Tabs/EffectiveTab.cs b/Penumbra/UI/Tabs/EffectiveTab.cs index 5f455189..a578e8d2 100644 --- a/Penumbra/UI/Tabs/EffectiveTab.cs +++ b/Penumbra/UI/Tabs/EffectiveTab.cs @@ -9,6 +9,7 @@ using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Collections; +using Penumbra.Collections.Manager; using Penumbra.Meta.Manipulations; using Penumbra.Mods; using Penumbra.String.Classes; @@ -43,7 +44,7 @@ public class EffectiveTab : ITab ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, _effectiveArrowLength); ImGui.TableSetupColumn("##file", ImGuiTableColumnFlags.WidthFixed, _effectiveRightTextLength); - DrawEffectiveRows(_collectionManager.Current, skips, height, + DrawEffectiveRows(_collectionManager.Active.Current, skips, height, _effectiveFilePathFilter.Length > 0 || _effectiveGamePathFilter.Length > 0); } diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index 37e0c24a..b454fa3b 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -16,7 +16,8 @@ using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI.ModsTab; using ModFileSystemSelector = Penumbra.UI.ModsTab.ModFileSystemSelector; - +using Penumbra.Collections.Manager; + namespace Penumbra.UI.Tabs; public class ModsTab : ITab @@ -85,12 +86,12 @@ public class ModsTab : ITab { Penumbra.Log.Error($"Exception thrown during ModPanel Render:\n{e}"); Penumbra.Log.Error($"{_modManager.Count} Mods\n" - + $"{_collectionManager.Current.AnonymizedName} Current Collection\n" - + $"{_collectionManager.Current.Settings.Count} Settings\n" + + $"{_collectionManager.Active.Current.AnonymizedName} Current Collection\n" + + $"{_collectionManager.Active.Current.Settings.Count} Settings\n" + $"{_selector.SortMode.Name} Sort Mode\n" + $"{_selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n" + $"{_selector.Selected?.Name ?? "NULL"} Selected Mod\n" - + $"{string.Join(", ", _collectionManager.Current.Inheritance.Select(c => c.AnonymizedName))} Inheritances\n" + + $"{string.Join(", ", _collectionManager.Active.Current.Inheritance.Select(c => c.AnonymizedName))} Inheritances\n" + $"{_selector.SelectedSettingCollection.AnonymizedName} Collection\n"); } } @@ -163,27 +164,27 @@ public class ModsTab : ITab _tutorial.OpenTutorial(BasicTutorialSteps.CollectionSelectors); - if (!_collectionManager.CurrentCollectionInUse) + if (!_collectionManager.Active.CurrentCollectionInUse) ImGuiUtil.DrawTextButton("The currently selected collection is not used in any way.", -Vector2.UnitX, Colors.PressEnterWarningBg); } private void DrawDefaultCollectionButton(Vector2 width) { - var name = $"{TutorialService.DefaultCollection} ({_collectionManager.Default.Name})"; - var isCurrent = _collectionManager.Default == _collectionManager.Current; - var isEmpty = _collectionManager.Default == ModCollection.Empty; + var name = $"{TutorialService.DefaultCollection} ({_collectionManager.Active.Default.Name})"; + var isCurrent = _collectionManager.Active.Default == _collectionManager.Active.Current; + var isEmpty = _collectionManager.Active.Default == ModCollection.Empty; var tt = isCurrent ? $"The current collection is already the configured {TutorialService.DefaultCollection}." : isEmpty ? $"The {TutorialService.DefaultCollection} is configured to be empty." : $"Set the {TutorialService.SelectedCollection} to the configured {TutorialService.DefaultCollection}."; if (ImGuiUtil.DrawDisabledButton(name, width, tt, isCurrent || isEmpty)) - _collectionManager.SetCollection(_collectionManager.Default, CollectionType.Current); + _collectionManager.Active.SetCollection(_collectionManager.Active.Default, CollectionType.Current); } private void DrawInheritedCollectionButton(Vector2 width) { var noModSelected = _selector.Selected == null; var collection = _selector.SelectedSettingCollection; - var modInherited = collection != _collectionManager.Current; + var modInherited = collection != _collectionManager.Active.Current; var (name, tt) = (noModSelected, modInherited) switch { (true, _) => ("Inherited Collection", "No mod selected."), @@ -192,7 +193,7 @@ public class ModsTab : ITab (false, false) => ("Not Inherited", "The selected mod does not inherit its settings."), }; if (ImGuiUtil.DrawDisabledButton(name, width, tt, noModSelected || !modInherited)) - _collectionManager.SetCollection(collection, CollectionType.Current); + _collectionManager.Active.SetCollection(collection, CollectionType.Current); } /// Get the correct size for the mod selector based on current config. diff --git a/Penumbra/UI/TutorialService.cs b/Penumbra/UI/TutorialService.cs index b470bcb9..2426160a 100644 --- a/Penumbra/UI/TutorialService.cs +++ b/Penumbra/UI/TutorialService.cs @@ -90,7 +90,7 @@ public class TutorialService + "In here, we can create new collections, delete collections, or make them inherit from each other.") .Register($"Initial Setup, Step 5: {SelectedCollection}", $"The {SelectedCollection} is the one we are currently editing. Any changes we make in our mod settings later in the next tab will edit this collection." - + $"We should already have a collection named {ModCollection.DefaultCollection} selected, and for our simple setup, we do not need to do anything here.\n\n") + + $"We should already have a collection named {ModCollection.DefaultCollectionName} selected, and for our simple setup, we do not need to do anything here.\n\n") .Register("Inheritance", "This is a more advanced feature. Click the help button for more information, but we will ignore this for now.") .Register($"Initial Setup, Step 6: {ActiveCollections}", @@ -99,7 +99,7 @@ public class TutorialService + $"The {SelectedCollection} is also active for technical reasons, while not necessarily being assigned to anything.\n\n" + "Open this now to continue.") .Register($"Initial Setup, Step 7: {DefaultCollection}", - $"The {DefaultCollection} - which should currently be set to a collection named {ModCollection.DefaultCollection} - is the main one.\n\n" + $"The {DefaultCollection} - which should currently be set to a collection named {ModCollection.DefaultCollectionName} - is the main one.\n\n" + $"As long as no more specific conditions apply to an object in the game, the mods from the {DefaultCollection} will be used.\n\n" + "This is also the collection you need to use for all mods that are not directly associated with any character in the game or the user interface, like music mods.") .Register("Interface Collection", diff --git a/Penumbra/Util/EventWrapper.cs b/Penumbra/Util/EventWrapper.cs index 2472e74d..cc374df1 100644 --- a/Penumbra/Util/EventWrapper.cs +++ b/Penumbra/Util/EventWrapper.cs @@ -4,32 +4,14 @@ using System.Linq; namespace Penumbra.Util; -public readonly struct EventWrapper : IDisposable +public abstract class EventWrapper : IDisposable where T : Delegate { - private readonly string _name; - private readonly List _event = new(); + private readonly string _name; + private readonly List<(object Subscriber, int Priority)> _event = new(); - public EventWrapper(string name) + protected EventWrapper(string name) => _name = name; - public void Invoke() - { - lock (_event) - { - foreach (var action in _event) - { - try - { - action.Invoke(); - } - catch (Exception ex) - { - Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}"); - } - } - } - } - public void Dispose() { lock (_event) @@ -38,345 +20,165 @@ public readonly struct EventWrapper : IDisposable } } - public event Action Event - { - add - { - lock (_event) - { - if (_event.All(a => a != value)) - _event.Add(value); - } - } - remove - { - lock (_event) - { - _event.Remove(value); - } - } - } -} - -public readonly struct EventWrapper : IDisposable -{ - private readonly string _name; - private readonly List> _event = new(); - - public EventWrapper(string name) - => _name = name; - - public void Invoke(T1 arg1) + public void Subscribe(T subscriber, int priority = 0) { lock (_event) { - foreach (var action in _event) + var existingIdx = _event.FindIndex(p => (T) p.Subscriber == subscriber); + var idx = _event.FindIndex(p => p.Priority > priority); + if (idx == existingIdx) + { + if (idx < 0) + _event.Add((subscriber, priority)); + else + _event[idx] = (subscriber, priority); + } + else + { + if (idx < 0) + _event.Add((subscriber, priority)); + else + _event.Insert(idx, (subscriber, priority)); + + if (existingIdx >= 0) + _event.RemoveAt(existingIdx < idx ? existingIdx : existingIdx + 1); + } + } + } + + public void Unsubscribe(T subscriber) + { + lock (_event) + { + var idx = _event.FindIndex(p => (T) p.Subscriber == subscriber); + if (idx >= 0) + _event.RemoveAt(idx); + } + } + + + protected static void Invoke(EventWrapper wrapper) + { + lock (wrapper._event) + { + foreach (var (action, _) in wrapper._event.AsEnumerable().Reverse()) { try { - action.Invoke(arg1); + ((Action)action).Invoke(); } catch (Exception ex) { - Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}"); + Penumbra.Log.Error($"[{wrapper._name}] Exception thrown during invocation:\n{ex}"); } } } } - public void Dispose() + protected static void Invoke(EventWrapper wrapper, T1 a) { - lock (_event) + lock (wrapper._event) { - _event.Clear(); - } - } - - public event Action Event - { - add - { - lock (_event) - { - if (_event.All(a => a != value)) - _event.Add(value); - } - } - remove - { - lock (_event) - { - _event.Remove(value); - } - } - } -} - -public readonly struct EventWrapper : IDisposable -{ - private readonly string _name; - private readonly List> _event = new(); - - public EventWrapper(string name) - => _name = name; - - public void Invoke(T1 arg1, T2 arg2) - { - lock (_event) - { - foreach (var action in _event) + foreach (var (action, _) in wrapper._event.AsEnumerable().Reverse()) { try { - action.Invoke(arg1, arg2); + ((Action)action).Invoke(a); } catch (Exception ex) { - Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}"); + Penumbra.Log.Error($"[{wrapper._name}] Exception thrown during invocation:\n{ex}"); } } } } - public void Dispose() + protected static void Invoke(EventWrapper wrapper, T1 a, T2 b) { - lock (_event) + lock (wrapper._event) { - _event.Clear(); - } - } - - public event Action Event - { - add - { - lock (_event) - { - if (_event.All(a => a != value)) - _event.Add(value); - } - } - remove - { - lock (_event) - { - _event.Remove(value); - } - } - } -} - -public readonly struct EventWrapper : IDisposable -{ - private readonly string _name; - private readonly List> _event = new(); - - public EventWrapper(string name) - => _name = name; - - public void Invoke(T1 arg1, T2 arg2, T3 arg3) - { - lock (_event) - { - foreach (var action in _event) + foreach (var (action, _) in wrapper._event.AsEnumerable().Reverse()) { try { - action.Invoke(arg1, arg2, arg3); + ((Action)action).Invoke(a, b); } catch (Exception ex) { - Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}"); + Penumbra.Log.Error($"[{wrapper._name}] Exception thrown during invocation:\n{ex}"); } } } } - public void Dispose() + protected static void Invoke(EventWrapper wrapper, T1 a, T2 b, T3 c) { - lock (_event) + lock (wrapper._event) { - _event.Clear(); - } - } - - public event Action Event - { - add - { - lock (_event) - { - if (_event.All(a => a != value)) - _event.Add(value); - } - } - remove - { - lock (_event) - { - _event.Remove(value); - } - } - } -} - -public readonly struct EventWrapper : IDisposable -{ - private readonly string _name; - private readonly List> _event = new(); - - public EventWrapper(string name) - => _name = name; - - public void Invoke(T1 arg1, T2 arg2, T3 arg3, T4 arg4) - { - lock (_event) - { - foreach (var action in _event) + foreach (var (action, _) in wrapper._event.AsEnumerable().Reverse()) { try { - action.Invoke(arg1, arg2, arg3, arg4); + ((Action)action).Invoke(a, b, c); } catch (Exception ex) { - Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}"); + Penumbra.Log.Error($"[{wrapper._name}] Exception thrown during invocation:\n{ex}"); } } } } - public void Dispose() + protected static void Invoke(EventWrapper wrapper, T1 a, T2 b, T3 c, T4 d) { - lock (_event) + lock (wrapper._event) { - _event.Clear(); - } - } - - public event Action Event - { - add - { - lock (_event) - { - if (_event.All(a => a != value)) - _event.Add(value); - } - } - remove - { - lock (_event) - { - _event.Remove(value); - } - } - } -} - -public readonly struct EventWrapper : IDisposable -{ - private readonly string _name; - private readonly List> _event = new(); - - public EventWrapper(string name) - => _name = name; - - public void Invoke(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) - { - lock (_event) - { - foreach (var action in _event) + foreach (var (action, _) in wrapper._event.AsEnumerable().Reverse()) { try { - action.Invoke(arg1, arg2, arg3, arg4, arg5); + ((Action)action).Invoke(a, b, c, d); } catch (Exception ex) { - Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}"); + Penumbra.Log.Error($"[{wrapper._name}] Exception thrown during invocation:\n{ex}"); } } } } - public void Dispose() + protected static void Invoke(EventWrapper wrapper, T1 a, T2 b, T3 c, T4 d, T5 e) { - lock (_event) + lock (wrapper._event) { - _event.Clear(); - } - } - - public event Action Event - { - add - { - lock (_event) - { - if (_event.All(a => a != value)) - _event.Add(value); - } - } - remove - { - lock (_event) - { - _event.Remove(value); - } - } - } -} - -public readonly struct EventWrapper : IDisposable -{ - private readonly string _name; - private readonly List> _event = new(); - - public EventWrapper(string name) - => _name = name; - - public void Invoke(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) - { - lock (_event) - { - foreach (var action in _event) + foreach (var (action, _) in wrapper._event.AsEnumerable().Reverse()) { try { - action.Invoke(arg1, arg2, arg3, arg4, arg5, arg6); + ((Action)action).Invoke(a, b, c, d, e); } catch (Exception ex) { - Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}"); + Penumbra.Log.Error($"[{wrapper._name}] Exception thrown during invocation:\n{ex}"); } } } } - public void Dispose() + protected static void Invoke(EventWrapper wrapper, T1 a, T2 b, T3 c, T4 d, T5 e, T6 f) { - lock (_event) + lock (wrapper._event) { - _event.Clear(); - } - } - - public event Action Event - { - add - { - lock (_event) + foreach (var (action, _) in wrapper._event.AsEnumerable().Reverse()) { - if (_event.All(a => a != value)) - _event.Add(value); - } - } - remove - { - lock (_event) - { - _event.Remove(value); + try + { + ((Action)action).Invoke(a, b, c, d, e, f); + } + catch (Exception ex) + { + Penumbra.Log.Error($"[{wrapper._name}] Exception thrown during invocation:\n{ex}"); + } } } }