using System; using System.Collections.Generic; using System.Linq; using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Filesystem; using Penumbra.Communication; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.Util; namespace Penumbra.Collections.Manager; /// /// ModCollections can inherit from an arbitrary number of other collections. /// This is transitive, so a collection A inheriting from B also inherits from everything B inherits. /// Circular dependencies are resolved by distinctness. /// public class InheritanceManager : IDisposable { public enum ValidInheritance { Valid, /// Can not inherit from self Self, /// Can not inherit from the empty collection Empty, /// Already inherited from Contained, /// Inheritance would lead to a circle. Circle, } private readonly CollectionStorage _storage; private readonly CommunicatorService _communicator; private readonly SaveService _saveService; private readonly ModStorage _modStorage; public InheritanceManager(CollectionStorage storage, SaveService saveService, CommunicatorService communicator, ModStorage modStorage) { _storage = storage; _saveService = saveService; _communicator = communicator; _modStorage = modStorage; ApplyInheritances(); _communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.InheritanceManager); } public void Dispose() { _communicator.CollectionChange.Unsubscribe(OnCollectionChange); } /// Check whether a collection can be inherited from. public static ValidInheritance CheckValidInheritance(ModCollection potentialInheritor, ModCollection? potentialParent) { if (potentialParent == null || ReferenceEquals(potentialParent, ModCollection.Empty)) return ValidInheritance.Empty; if (ReferenceEquals(potentialParent, potentialInheritor)) return ValidInheritance.Self; if (potentialInheritor.DirectlyInheritsFrom.Contains(potentialParent)) return ValidInheritance.Contained; if (ModCollection.InheritedCollections(potentialParent).Any(c => ReferenceEquals(c, potentialInheritor))) return ValidInheritance.Circle; return ValidInheritance.Valid; } /// /// Add a new collection to the inheritance list. /// We do not check if this collection would be visited before, /// only that it is unique in the list itself. /// public bool AddInheritance(ModCollection inheritor, ModCollection parent) => AddInheritance(inheritor, parent, true); /// Remove an existing inheritance from a collection. public void RemoveInheritance(ModCollection inheritor, int idx) { var parent = inheritor.DirectlyInheritsFrom[idx]; ((List)inheritor.DirectlyInheritsFrom).RemoveAt(idx); ((List)parent.DirectParentOf).Remove(inheritor); _saveService.DelaySave(new ModCollectionSave(_modStorage, inheritor)); _communicator.CollectionInheritanceChanged.Invoke(inheritor, false); RecurseInheritanceChanges(inheritor); Penumbra.Log.Debug($"Removed {parent.AnonymizedName} from {inheritor.AnonymizedName} inheritances."); } /// Order in the inheritance list is relevant. public void MoveInheritance(ModCollection inheritor, int from, int to) { if (!((List)inheritor.DirectlyInheritsFrom).Move(from, to)) return; _saveService.DelaySave(new ModCollectionSave(_modStorage, inheritor)); _communicator.CollectionInheritanceChanged.Invoke(inheritor, false); RecurseInheritanceChanges(inheritor); Penumbra.Log.Debug($"Moved {inheritor.AnonymizedName}s inheritance {from} to {to}."); } /// private bool AddInheritance(ModCollection inheritor, ModCollection parent, bool invokeEvent) { if (CheckValidInheritance(inheritor, parent) != ValidInheritance.Valid) return false; ((List)inheritor.DirectlyInheritsFrom).Add(parent); ((List)parent.DirectParentOf).Add(inheritor); if (invokeEvent) { _saveService.DelaySave(new ModCollectionSave(_modStorage, inheritor)); _communicator.CollectionInheritanceChanged.Invoke(inheritor, false); RecurseInheritanceChanges(inheritor); } Penumbra.Log.Debug($"Added {parent.AnonymizedName} to {inheritor.AnonymizedName} inheritances."); return true; } /// /// 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, directParents, changes) in _storage.ConsumeInheritanceNames()) { var localChanges = changes; foreach (var parent in directParents) { if (AddInheritance(collection, parent, false)) continue; localChanges = true; Penumbra.ChatService.NotificationMessage($"{collection.Name} can not inherit from {parent.Name}, removed.", "Warning", NotificationType.Warning); } if (localChanges) _saveService.ImmediateSave(new ModCollectionSave(_modStorage, collection)); } } private void OnCollectionChange(CollectionType collectionType, ModCollection? old, ModCollection? newCollection, string _3) { if (collectionType is not CollectionType.Inactive || old == null) return; foreach (var c in _storage) { var inheritedIdx = c.DirectlyInheritsFrom.IndexOf(old); if (inheritedIdx >= 0) RemoveInheritance(c, inheritedIdx); ((List)c.DirectParentOf).Remove(old); } } private void RecurseInheritanceChanges(ModCollection newInheritor) { foreach (var inheritor in newInheritor.DirectParentOf) { _communicator.CollectionInheritanceChanged.Invoke(inheritor, true); RecurseInheritanceChanges(inheritor); } } }