diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs index 3dc900fc..4027975b 100644 --- a/Penumbra/Api/Api/ModSettingsApi.cs +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -1,4 +1,5 @@ using OtterGui; +using OtterGui.Log; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Api.Helpers; @@ -24,18 +25,20 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable private readonly CollectionManager _collectionManager; private readonly CollectionEditor _collectionEditor; private readonly CommunicatorService _communicator; + private readonly ApiHelpers _helpers; public ModSettingsApi(CollectionResolver collectionResolver, ModManager modManager, CollectionManager collectionManager, CollectionEditor collectionEditor, - CommunicatorService communicator) + CommunicatorService communicator, ApiHelpers helpers) { _collectionResolver = collectionResolver; _modManager = modManager; _collectionManager = collectionManager; _collectionEditor = collectionEditor; _communicator = communicator; + _helpers = helpers; _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ApiModSettings); _communicator.ModSettingChanged.Subscribe(OnModSettingChange, Communication.ModSettingChanged.Priority.Api); _communicator.ModOptionChanged.Subscribe(OnModOptionEdited, ModOptionChanged.Priority.Api); @@ -63,11 +66,6 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable return new AvailableModSettings(dict); } - public Dictionary? GetAvailableModSettingsBase(string modDirectory, string modName) - => _modManager.TryGetMod(modDirectory, modName, out var mod) - ? mod.Groups.ToDictionary(g => g.Name, g => (g.Options.Select(o => o.Name).ToArray(), (int)g.Type)) - : null; - public (PenumbraApiEc, (bool, int, Dictionary>, bool)?) GetCurrentModSettings(Guid collectionId, string modDirectory, string modName, bool ignoreInheritance) { @@ -80,14 +78,14 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable var settings = collection.Identity.Id == Guid.Empty ? null : ignoreInheritance - ? collection.Settings[mod.Index] - : collection[mod.Index].Settings; + ? collection.GetOwnSettings(mod.Index) + : collection.GetInheritedSettings(mod.Index).Settings; if (settings == null) return (PenumbraApiEc.Success, null); var (enabled, priority, dict) = settings.ConvertToShareable(mod); return (PenumbraApiEc.Success, - (enabled, priority.Value, dict, collection.Settings[mod.Index] == null)); + (enabled, priority.Value, dict, collection.GetOwnSettings(mod.Index) is null)); } public PenumbraApiEc TryInheritMod(Guid collectionId, string modDirectory, string modName, bool inherit) @@ -211,11 +209,147 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable return ApiHelpers.Return(PenumbraApiEc.Success, args); } + public PenumbraApiEc SetTemporaryModSetting(Guid collectionId, string modDirectory, string modName, bool enabled, int priority, + IReadOnlyDictionary> options, string source, int key) + { + var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Enabled", enabled, + "Priority", priority, "Options", options, "Source", source, "Key", key); + if (!_collectionManager.Storage.ById(collectionId, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + return SetTemporaryModSetting(args, collection, modDirectory, modName, enabled, priority, options, source, key); + } + + public PenumbraApiEc TemporaryModSettingsPlayer(int objectIndex, string modDirectory, string modName, bool enabled, int priority, + IReadOnlyDictionary> options, string source, int key) + { + return PenumbraApiEc.Success; + } + + private PenumbraApiEc SetTemporaryModSetting(in LazyString args, ModCollection collection, string modDirectory, string modName, + bool enabled, int priority, + IReadOnlyDictionary> options, string source, int key) + { + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) + return ApiHelpers.Return(PenumbraApiEc.ModMissing, args); + + if (collection.GetTempSettings(mod.Index) is { } settings && settings.Lock != 0 && settings.Lock != key) + return ApiHelpers.Return(PenumbraApiEc.TemporarySettingDisallowed, args); + + settings = new TemporaryModSettings + { + Enabled = enabled, + Priority = new ModPriority(priority), + Lock = key, + Source = source, + Settings = SettingList.Default(mod), + }; + + foreach (var (groupName, optionNames) in options) + { + var groupIdx = mod.Groups.IndexOf(g => g.Name == groupName); + if (groupIdx < 0) + return ApiHelpers.Return(PenumbraApiEc.OptionGroupMissing, args); + + var setting = Setting.Zero; + switch (mod.Groups[groupIdx]) + { + case { Behaviour: GroupDrawBehaviour.SingleSelection } single: + { + var optionIdx = optionNames.Count == 0 ? -1 : single.Options.IndexOf(o => o.Name == optionNames[^1]); + if (optionIdx < 0) + return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); + + setting = Setting.Single(optionIdx); + break; + } + case { Behaviour: GroupDrawBehaviour.MultiSelection } multi: + { + foreach (var name in optionNames) + { + var optionIdx = multi.Options.IndexOf(o => o.Name == name); + if (optionIdx < 0) + return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); + + setting |= Setting.Multi(optionIdx); + } + + break; + } + } + + settings.Settings[groupIdx] = setting; + } + + collection.Settings.SetTemporary(mod.Index, settings); + return ApiHelpers.Return(PenumbraApiEc.Success, args); + } + + public PenumbraApiEc RemoveTemporaryModSettings(Guid collectionId, string modDirectory, string modName, int key) + { + var args = ApiHelpers.Args("CollectionId", collectionId, "ModDirectory", modDirectory, "ModName", modName, "Key", key); + if (!_collectionManager.Storage.ById(collectionId, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + return RemoveTemporaryModSettings(args, collection, modDirectory, modName, key); + } + + private PenumbraApiEc RemoveTemporaryModSettings(in LazyString args, ModCollection collection, string modDirectory, string modName, int key) + { + if (!_modManager.TryGetMod(modDirectory, modName, out var mod)) + return ApiHelpers.Return(PenumbraApiEc.ModMissing, args); + + if (collection.GetTempSettings(mod.Index) is not { } settings) + return ApiHelpers.Return(PenumbraApiEc.NothingChanged, args); + + if (settings.Lock != 0 && settings.Lock != key) + return ApiHelpers.Return(PenumbraApiEc.TemporarySettingDisallowed, args); + + collection.Settings.SetTemporary(mod.Index, null); + return ApiHelpers.Return(PenumbraApiEc.Success, args); + } + + public PenumbraApiEc RemoveTemporaryModSettingsPlayer(int objectIndex, string modDirectory, string modName, int key) + { + return PenumbraApiEc.Success; + } + + public PenumbraApiEc RemoveAllTemporaryModSettings(Guid collectionId, int key) + { + var args = ApiHelpers.Args("CollectionId", collectionId, "Key", key); + if (!_collectionManager.Storage.ById(collectionId, out var collection)) + return ApiHelpers.Return(PenumbraApiEc.CollectionMissing, args); + + return RemoveAllTemporaryModSettings(args, collection, key); + } + + public PenumbraApiEc RemoveAllTemporaryModSettingsPlayer(int objectIndex, int key) + { + return PenumbraApiEc.Success; + } + + private PenumbraApiEc RemoveAllTemporaryModSettings(in LazyString args, ModCollection collection, int key) + { + var numRemoved = 0; + for (var i = 0; i < collection.Settings.Count; ++i) + { + if (collection.GetTempSettings(i) is { } settings && (settings.Lock == 0 || settings.Lock == key)) + { + collection.Settings.SetTemporary(i, null); + ++numRemoved; + } + } + + return ApiHelpers.Return(numRemoved > 0 ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged, args); + } + + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void TriggerSettingEdited(Mod mod) { var collection = _collectionResolver.PlayerCollection(); - var (settings, parent) = collection[mod.Index]; + var (settings, parent) = collection.GetActualSettings(mod.Index); if (settings is { Enabled: true }) ModSettingChanged?.Invoke(ModSettingChange.Edited, collection.Identity.Id, mod.Identifier, parent != collection); } diff --git a/Penumbra/Collections/Cache/CollectionCache.cs b/Penumbra/Collections/Cache/CollectionCache.cs index ad902aac..8ca9aa36 100644 --- a/Penumbra/Collections/Cache/CollectionCache.cs +++ b/Penumbra/Collections/Cache/CollectionCache.cs @@ -260,7 +260,7 @@ public sealed class CollectionCache : IDisposable if (mod.Index < 0) return mod.GetData(); - var settings = _collection[mod.Index].Settings; + var settings = _collection.GetActualSettings(mod.Index).Settings; return settings is not { Enabled: true } ? AppliedModData.Empty : mod.GetData(settings); @@ -342,8 +342,8 @@ public sealed class CollectionCache : IDisposable // Returns if the added mod takes priority before the existing mod. private bool AddConflict(object data, IMod addedMod, IMod existingMod) { - var addedPriority = addedMod.Index >= 0 ? _collection[addedMod.Index].Settings!.Priority : addedMod.Priority; - var existingPriority = existingMod.Index >= 0 ? _collection[existingMod.Index].Settings!.Priority : existingMod.Priority; + var addedPriority = addedMod.Index >= 0 ? _collection.GetActualSettings(addedMod.Index).Settings!.Priority : addedMod.Priority; + var existingPriority = existingMod.Index >= 0 ? _collection.GetActualSettings(existingMod.Index).Settings!.Priority : existingMod.Priority; if (existingPriority < addedPriority) { diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index 0a851154..839c0376 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -231,11 +231,11 @@ public class CollectionCacheManager : IDisposable, IService { case ModPathChangeType.Deleted: case ModPathChangeType.StartingReload: - foreach (var collection in _storage.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) + foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(mod.Index).Settings?.Enabled == true)) collection._cache!.RemoveMod(mod, true); break; case ModPathChangeType.Moved: - foreach (var collection in _storage.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) + foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(mod.Index).Settings?.Enabled == true)) collection._cache!.ReloadMod(mod, true); break; } @@ -246,7 +246,7 @@ public class CollectionCacheManager : IDisposable, IService if (type is not (ModPathChangeType.Added or ModPathChangeType.Reloaded)) return; - foreach (var collection in _storage.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) + foreach (var collection in _storage.Where(c => c.HasCache && c.GetActualSettings(mod.Index).Settings?.Enabled == true)) collection._cache!.AddMod(mod, true); } @@ -273,7 +273,7 @@ public class CollectionCacheManager : IDisposable, IService { if (type is ModOptionChangeType.PrepareChange) { - foreach (var collection in _storage.Where(collection => collection.HasCache && collection[mod.Index].Settings is { Enabled: true })) + foreach (var collection in _storage.Where(collection => collection.HasCache && collection.GetActualSettings(mod.Index).Settings is { Enabled: true })) collection._cache!.RemoveMod(mod, false); return; @@ -284,7 +284,7 @@ public class CollectionCacheManager : IDisposable, IService if (!recomputeList) return; - foreach (var collection in _storage.Where(collection => collection.HasCache && collection[mod.Index].Settings is { Enabled: true })) + foreach (var collection in _storage.Where(collection => collection.HasCache && collection.GetActualSettings(mod.Index).Settings is { Enabled: true })) { if (justAdd) collection._cache!.AddMod(mod, true); @@ -317,7 +317,7 @@ public class CollectionCacheManager : IDisposable, IService cache.AddMod(mod!, true); else if (oldValue == Setting.True) cache.RemoveMod(mod!, true); - else if (collection[mod!.Index].Settings?.Enabled == true) + else if (collection.GetActualSettings(mod!.Index).Settings?.Enabled == true) cache.ReloadMod(mod!, true); else cache.RemoveMod(mod!, true); @@ -329,8 +329,8 @@ public class CollectionCacheManager : IDisposable, IService break; case ModSettingChange.Setting: - if (collection[mod!.Index].Settings?.Enabled == true) - cache.ReloadMod(mod!, true); + if (collection.GetActualSettings(mod!.Index).Settings?.Enabled == true) + cache.ReloadMod(mod, true); break; case ModSettingChange.MultiInheritance: diff --git a/Penumbra/Collections/Manager/ActiveCollections.cs b/Penumbra/Collections/Manager/ActiveCollections.cs index 07fcb430..2ced8ad6 100644 --- a/Penumbra/Collections/Manager/ActiveCollections.cs +++ b/Penumbra/Collections/Manager/ActiveCollections.cs @@ -282,7 +282,7 @@ public class ActiveCollections : ISavable, IDisposable, IService .Prepend(Interface) .Prepend(Default) .Concat(Individuals.Assignments.Select(kvp => kvp.Collection)) - .SelectMany(c => c.GetFlattenedInheritance()).Contains(Current); + .SelectMany(c => c.Inheritance.FlatHierarchy).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) diff --git a/Penumbra/Collections/Manager/CollectionEditor.cs b/Penumbra/Collections/Manager/CollectionEditor.cs index caff2c86..66578a95 100644 --- a/Penumbra/Collections/Manager/CollectionEditor.cs +++ b/Penumbra/Collections/Manager/CollectionEditor.cs @@ -26,12 +26,12 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu /// public bool SetModState(ModCollection collection, Mod mod, bool newValue) { - var oldValue = collection.Settings[mod.Index]?.Enabled ?? collection[mod.Index].Settings?.Enabled ?? false; + var oldValue = collection.GetInheritedSettings(mod.Index).Settings?.Enabled ?? false; if (newValue == oldValue) return false; var inheritance = FixInheritance(collection, mod, false); - ((List)collection.Settings)[mod.Index]!.Enabled = newValue; + collection.GetOwnSettings(mod.Index)!.Enabled = newValue; InvokeChange(collection, ModSettingChange.EnableState, mod, inheritance ? Setting.Indefinite : newValue ? Setting.False : Setting.True, 0); return true; @@ -55,13 +55,13 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu var changes = false; foreach (var mod in mods) { - var oldValue = collection.Settings[mod.Index]?.Enabled; + var oldValue = collection.GetOwnSettings(mod.Index)?.Enabled; if (newValue == oldValue) continue; FixInheritance(collection, mod, false); - ((List)collection.Settings)[mod.Index]!.Enabled = newValue; - changes = true; + collection.GetOwnSettings(mod.Index)!.Enabled = newValue; + changes = true; } if (!changes) @@ -76,12 +76,12 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu /// public bool SetModPriority(ModCollection collection, Mod mod, ModPriority newValue) { - var oldValue = collection.Settings[mod.Index]?.Priority ?? collection[mod.Index].Settings?.Priority ?? ModPriority.Default; + var oldValue = collection.GetInheritedSettings(mod.Index).Settings?.Priority ?? ModPriority.Default; if (newValue == oldValue) return false; var inheritance = FixInheritance(collection, mod, false); - ((List)collection.Settings)[mod.Index]!.Priority = newValue; + collection.GetOwnSettings(mod.Index)!.Priority = newValue; InvokeChange(collection, ModSettingChange.Priority, mod, inheritance ? Setting.Indefinite : oldValue.AsSetting, 0); return true; } @@ -92,15 +92,13 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu /// public bool SetModSetting(ModCollection collection, Mod mod, int groupIdx, Setting newValue) { - var settings = collection.Settings[mod.Index] != null - ? collection.Settings[mod.Index]!.Settings - : collection[mod.Index].Settings?.Settings; + var settings = collection.GetInheritedSettings(mod.Index).Settings?.Settings; var oldValue = settings?[groupIdx] ?? mod.Groups[groupIdx].DefaultSettings; if (oldValue == newValue) return false; var inheritance = FixInheritance(collection, mod, false); - ((List)collection.Settings)[mod.Index]!.SetValue(mod, groupIdx, newValue); + collection.GetOwnSettings(mod.Index)!.SetValue(mod, groupIdx, newValue); InvokeChange(collection, ModSettingChange.Setting, mod, inheritance ? Setting.Indefinite : oldValue, groupIdx); return true; } @@ -115,10 +113,10 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu // If it does not exist, check unused settings. // If it does not exist and has no unused settings, also use null. ModSettings.SavedSettings? savedSettings = sourceMod != null - ? collection.Settings[sourceMod.Index] != null - ? new ModSettings.SavedSettings(collection.Settings[sourceMod.Index]!, sourceMod) + ? collection.GetOwnSettings(sourceMod.Index) is { } ownSettings + ? new ModSettings.SavedSettings(ownSettings, sourceMod) : null - : collection.UnusedSettings.TryGetValue(sourceName, out var s) + : collection.Settings.Unused.TryGetValue(sourceName, out var s) ? s : null; @@ -148,10 +146,10 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu // or remove any unused settings for the target if they are inheriting. if (savedSettings != null) { - ((Dictionary)collection.UnusedSettings)[targetName] = savedSettings.Value; + ((Dictionary)collection.Settings.Unused)[targetName] = savedSettings.Value; saveService.QueueSave(new ModCollectionSave(modStorage, collection)); } - else if (((Dictionary)collection.UnusedSettings).Remove(targetName)) + else if (((Dictionary)collection.Settings.Unused).Remove(targetName)) { saveService.QueueSave(new ModCollectionSave(modStorage, collection)); } @@ -166,12 +164,12 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu /// private static bool FixInheritance(ModCollection collection, Mod mod, bool inherit) { - var settings = collection.Settings[mod.Index]; + var settings = collection.GetOwnSettings(mod.Index); if (inherit == (settings == null)) return false; - ((List)collection.Settings)[mod.Index] = - inherit ? null : collection[mod.Index].Settings?.DeepCopy() ?? ModSettings.DefaultSettings(mod); + ModSettings? settings1 = inherit ? null : collection.GetInheritedSettings(mod.Index).Settings?.DeepCopy() ?? ModSettings.DefaultSettings(mod); + collection.Settings.Set(mod.Index, settings1); return true; } @@ -188,7 +186,7 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] private void RecurseInheritors(ModCollection directParent, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx) { - foreach (var directInheritor in directParent.DirectParentOf) + foreach (var directInheritor in directParent.Inheritance.DirectlyInheritedBy) { switch (type) { @@ -197,7 +195,7 @@ public class CollectionEditor(SaveService saveService, CommunicatorService commu communicator.ModSettingChanged.Invoke(directInheritor, type, null, oldValue, groupIdx, true); break; default: - if (directInheritor.Settings[mod!.Index] == null) + if (directInheritor.GetOwnSettings(mod!.Index) == null) communicator.ModSettingChanged.Invoke(directInheritor, type, mod, oldValue, groupIdx, true); break; } diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index 2ed395ae..e19acd35 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -41,8 +41,8 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer public ModCollection CreateFromData(Guid id, string name, int version, Dictionary allSettings, IReadOnlyList inheritances) { - var newCollection = ModCollection.CreateFromData(_saveService, _modStorage, new ModCollectionIdentity(id, CurrentCollectionId, name, Count), version, allSettings, - inheritances); + var newCollection = ModCollection.CreateFromData(_saveService, _modStorage, + new ModCollectionIdentity(id, CurrentCollectionId, name, Count), version, allSettings, inheritances); _collectionsByLocal[CurrentCollectionId] = newCollection; CurrentCollectionId += 1; return newCollection; @@ -196,8 +196,8 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer /// Remove all settings for not currently-installed mods from the given collection. public void CleanUnavailableSettings(ModCollection collection) { - var any = collection.UnusedSettings.Count > 0; - ((Dictionary)collection.UnusedSettings).Clear(); + var any = collection.Settings.Unused.Count > 0; + ((Dictionary)collection.Settings.Unused).Clear(); if (any) _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); } @@ -205,7 +205,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer /// Remove a specific setting for not currently-installed mods from the given collection. public void CleanUnavailableSetting(ModCollection collection, string? setting) { - if (setting != null && ((Dictionary)collection.UnusedSettings).Remove(setting)) + if (setting != null && ((Dictionary)collection.Settings.Unused).Remove(setting)) _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); } @@ -307,7 +307,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer private void OnModDiscoveryStarted() { foreach (var collection in this) - collection.PrepareModDiscovery(_modStorage); + collection.Settings.PrepareModDiscovery(_modStorage); } /// Restore all settings in all collections to mods. @@ -315,7 +315,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer { // Re-apply all mod settings. foreach (var collection in this) - collection.ApplyModSettings(_saveService, _modStorage); + collection.Settings.ApplyModSettings(collection, _saveService, _modStorage); } /// Add or remove a mod from all collections, or re-save all collections where the mod has settings. @@ -326,21 +326,22 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer { case ModPathChangeType.Added: foreach (var collection in this) - collection.AddMod(mod); + collection.Settings.AddMod(mod); break; case ModPathChangeType.Deleted: foreach (var collection in this) - collection.RemoveMod(mod); + collection.Settings.RemoveMod(mod); break; case ModPathChangeType.Moved: - foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null)) + foreach (var collection in this.Where(collection => collection.GetOwnSettings(mod.Index) != null)) _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); break; case ModPathChangeType.Reloaded: foreach (var collection in this) { - if (collection.Settings[mod.Index]?.Settings.FixAll(mod) ?? false) + if (collection.GetOwnSettings(mod.Index)?.Settings.FixAll(mod) ?? false) _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); + collection.Settings.SetTemporary(mod.Index, null); } break; @@ -357,8 +358,9 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer foreach (var collection in this) { - if (collection.Settings[mod.Index]?.HandleChanges(type, mod, group, option, movedToIdx) ?? false) + if (collection.GetOwnSettings(mod.Index)?.HandleChanges(type, mod, group, option, movedToIdx) ?? false) _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); + collection.Settings.SetTemporary(mod.Index, null); } } @@ -370,7 +372,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable, ISer foreach (var collection in this) { - var (settings, _) = collection[mod.Index]; + var (settings, _) = collection.GetActualSettings(mod.Index); if (settings is { Enabled: true }) collection.Counters.IncrementChange(); } diff --git a/Penumbra/Collections/Manager/InheritanceManager.cs b/Penumbra/Collections/Manager/InheritanceManager.cs index e003ad6b..5e361bde 100644 --- a/Penumbra/Collections/Manager/InheritanceManager.cs +++ b/Penumbra/Collections/Manager/InheritanceManager.cs @@ -1,7 +1,6 @@ using Dalamud.Interface.ImGuiNotification; using OtterGui; using OtterGui.Classes; -using OtterGui.Filesystem; using OtterGui.Services; using Penumbra.Communication; using Penumbra.Mods.Manager; @@ -63,10 +62,10 @@ public class InheritanceManager : IDisposable, IService if (ReferenceEquals(potentialParent, potentialInheritor)) return ValidInheritance.Self; - if (potentialInheritor.DirectlyInheritsFrom.Contains(potentialParent)) + if (potentialInheritor.Inheritance.DirectlyInheritsFrom.Contains(potentialParent)) return ValidInheritance.Contained; - if (ModCollection.InheritedCollections(potentialParent).Any(c => ReferenceEquals(c, potentialInheritor))) + if (potentialParent.Inheritance.FlatHierarchy.Any(c => ReferenceEquals(c, potentialInheritor))) return ValidInheritance.Circle; return ValidInheritance.Valid; @@ -83,24 +82,22 @@ public class InheritanceManager : IDisposable, IService /// 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); + var parent = inheritor.Inheritance.RemoveInheritanceAt(inheritor, idx); _saveService.QueueSave(new ModCollectionSave(_modStorage, inheritor)); _communicator.CollectionInheritanceChanged.Invoke(inheritor, false); - RecurseInheritanceChanges(inheritor); + RecurseInheritanceChanges(inheritor, true); Penumbra.Log.Debug($"Removed {parent.Identity.AnonymizedName} from {inheritor.Identity.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)) + if (!inheritor.Inheritance.MoveInheritance(inheritor, from, to)) return; _saveService.QueueSave(new ModCollectionSave(_modStorage, inheritor)); _communicator.CollectionInheritanceChanged.Invoke(inheritor, false); - RecurseInheritanceChanges(inheritor); + RecurseInheritanceChanges(inheritor, true); Penumbra.Log.Debug($"Moved {inheritor.Identity.AnonymizedName}s inheritance {from} to {to}."); } @@ -110,15 +107,15 @@ public class InheritanceManager : IDisposable, IService if (CheckValidInheritance(inheritor, parent) != ValidInheritance.Valid) return false; - ((List)inheritor.DirectlyInheritsFrom).Add(parent); - ((List)parent.DirectParentOf).Add(inheritor); + inheritor.Inheritance.AddInheritance(inheritor, parent); if (invokeEvent) { _saveService.QueueSave(new ModCollectionSave(_modStorage, inheritor)); _communicator.CollectionInheritanceChanged.Invoke(inheritor, false); - RecurseInheritanceChanges(inheritor); } + RecurseInheritanceChanges(inheritor, invokeEvent); + Penumbra.Log.Debug($"Added {parent.Identity.AnonymizedName} to {inheritor.Identity.AnonymizedName} inheritances."); return true; } @@ -131,11 +128,11 @@ public class InheritanceManager : IDisposable, IService { foreach (var collection in _storage) { - if (collection.InheritanceByName == null) + if (collection.Inheritance.ConsumeNames() is not { } byName) continue; var changes = false; - foreach (var subCollectionName in collection.InheritanceByName) + foreach (var subCollectionName in byName) { if (Guid.TryParse(subCollectionName, out var guid) && _storage.ById(guid, out var subCollection)) { @@ -143,7 +140,8 @@ public class InheritanceManager : IDisposable, IService continue; changes = true; - Penumbra.Messager.NotificationMessage($"{collection.Identity.Name} can not inherit from {subCollection.Identity.Name}, removed.", + Penumbra.Messager.NotificationMessage( + $"{collection.Identity.Name} can not inherit from {subCollection.Identity.Name}, removed.", NotificationType.Warning); } else if (_storage.ByName(subCollectionName, out subCollection)) @@ -153,7 +151,8 @@ public class InheritanceManager : IDisposable, IService if (AddInheritance(collection, subCollection, false)) continue; - Penumbra.Messager.NotificationMessage($"{collection.Identity.Name} can not inherit from {subCollection.Identity.Name}, removed.", + Penumbra.Messager.NotificationMessage( + $"{collection.Identity.Name} can not inherit from {subCollection.Identity.Name}, removed.", NotificationType.Warning); } else @@ -165,7 +164,6 @@ public class InheritanceManager : IDisposable, IService } } - collection.InheritanceByName = null; if (changes) _saveService.ImmediateSave(new ModCollectionSave(_modStorage, collection)); } @@ -178,20 +176,22 @@ public class InheritanceManager : IDisposable, IService foreach (var c in _storage) { - var inheritedIdx = c.DirectlyInheritsFrom.IndexOf(old); + var inheritedIdx = c.Inheritance.DirectlyInheritsFrom.IndexOf(old); if (inheritedIdx >= 0) RemoveInheritance(c, inheritedIdx); - ((List)c.DirectParentOf).Remove(old); + c.Inheritance.RemoveChild(old); } } - private void RecurseInheritanceChanges(ModCollection newInheritor) + private void RecurseInheritanceChanges(ModCollection newInheritor, bool invokeEvent) { - foreach (var inheritor in newInheritor.DirectParentOf) + foreach (var inheritor in newInheritor.Inheritance.DirectlyInheritedBy) { - _communicator.CollectionInheritanceChanged.Invoke(inheritor, true); - RecurseInheritanceChanges(inheritor); + ModCollectionInheritance.UpdateFlattenedInheritance(inheritor); + RecurseInheritanceChanges(inheritor, invokeEvent); + if (invokeEvent) + _communicator.CollectionInheritanceChanged.Invoke(inheritor, true); } } } diff --git a/Penumbra/Collections/Manager/ModCollectionMigration.cs b/Penumbra/Collections/Manager/ModCollectionMigration.cs index fe61285d..7db375f7 100644 --- a/Penumbra/Collections/Manager/ModCollectionMigration.cs +++ b/Penumbra/Collections/Manager/ModCollectionMigration.cs @@ -26,12 +26,12 @@ internal static class ModCollectionMigration // Remove all completely defaulted settings from active and inactive mods. for (var i = 0; i < collection.Settings.Count; ++i) { - if (SettingIsDefaultV0(collection.Settings[i])) - ((List)collection.Settings)[i] = null; + if (SettingIsDefaultV0(collection.GetOwnSettings(i))) + collection.Settings.SetAll(i, FullModSettings.Empty); } - foreach (var (key, _) in collection.UnusedSettings.Where(kvp => SettingIsDefaultV0(kvp.Value)).ToList()) - ((Dictionary)collection.UnusedSettings).Remove(key); + foreach (var (key, _) in collection.Settings.Unused.Where(kvp => SettingIsDefaultV0(kvp.Value)).ToList()) + collection.Settings.RemoveUnused(key); return true; } diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 9b33c1f4..69f82458 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -1,4 +1,3 @@ -using Penumbra.Mods; using Penumbra.Mods.Manager; using Penumbra.Collections.Manager; using Penumbra.Mods.Settings; @@ -22,70 +21,74 @@ public partial class ModCollection /// Create the always available Empty Collection that will always sit at index 0, /// can not be deleted and does never create a cache. /// - public static readonly ModCollection Empty = new(ModCollectionIdentity.Empty, 0, CurrentVersion, [], [], []); + public static readonly ModCollection Empty = new(ModCollectionIdentity.Empty, 0, CurrentVersion, new ModSettingProvider(), + new ModCollectionInheritance()); public ModCollectionIdentity Identity; public override string ToString() => Identity.ToString(); - public CollectionCounters Counters; + public readonly ModSettingProvider Settings; + public ModCollectionInheritance Inheritance; + public CollectionCounters Counters; - /// - /// If a ModSetting is null, it can be inherited from other collections. - /// If no collection provides a setting for the mod, it is just disabled. - /// - public readonly IReadOnlyList Settings; - /// Settings for deleted mods will be kept via the mods identifier (directory name). - public readonly IReadOnlyDictionary UnusedSettings; - - /// Inheritances stored before they can be applied. - public IReadOnlyList? InheritanceByName; - - /// Contains all direct parent collections this collection inherits settings from. - public readonly IReadOnlyList DirectlyInheritsFrom; - - /// Contains all direct child collections that inherit from this collection. - public readonly IReadOnlyList DirectParentOf = new List(); - - /// All inherited collections in application order without filtering for duplicates. - public static IEnumerable InheritedCollections(ModCollection collection) - => collection.DirectlyInheritsFrom.SelectMany(InheritedCollections).Prepend(collection); - - /// - /// Iterate over all collections inherited from in depth-first order. - /// Skip already visited collections to avoid circular dependencies. - /// - public IEnumerable GetFlattenedInheritance() - => InheritedCollections(this).Distinct(); - - /// - /// Obtain the actual settings for a given mod via index. - /// Also returns the collection the settings are taken from. - /// If no collection provides settings for this mod, this collection is returned together with null. - /// - public (ModSettings? Settings, ModCollection Collection) this[Index idx] + public ModSettings? GetOwnSettings(Index idx) { - get + if (Identity.Index <= 0) + return ModSettings.Empty; + + return Settings.Settings[idx].Settings; + } + + public TemporaryModSettings? GetTempSettings(Index idx) + { + if (Identity.Index <= 0) + return null; + + return Settings.Settings[idx].TempSettings; + } + + public (ModSettings? Settings, ModCollection Collection) GetInheritedSettings(Index idx) + { + if (Identity.Index <= 0) + return (ModSettings.Empty, this); + + foreach (var collection in Inheritance.FlatHierarchy) { - if (Identity.Index <= 0) - return (ModSettings.Empty, this); - - foreach (var collection in GetFlattenedInheritance()) - { - var settings = collection.Settings[idx]; - if (settings != null) - return (settings, collection); - } - - return (null, this); + var settings = collection.Settings.Settings[idx].Settings; + if (settings != null) + return (settings, collection); } + + return (null, this); + } + + public (ModSettings? Settings, ModCollection Collection) GetActualSettings(Index idx) + { + if (Identity.Index <= 0) + return (ModSettings.Empty, this); + + // Check temp settings. + var ownTempSettings = Settings.Settings[idx].Resolve(); + if (ownTempSettings != null) + return (ownTempSettings, this); + + // Ignore temp settings for inherited collections. + foreach (var collection in Inheritance.FlatHierarchy.Skip(1)) + { + var settings = collection.Settings.Settings[idx].Settings; + if (settings != null) + return (settings, collection); + } + + return (null, this); } /// Evaluates all settings along the whole inheritance tree. public IEnumerable ActualSettings - => Enumerable.Range(0, Settings.Count).Select(i => this[i].Settings); + => Enumerable.Range(0, Settings.Count).Select(i => GetActualSettings(i).Settings); /// /// Constructor for duplication. Deep copies all settings and parent collections and adds the new collection to their children lists. @@ -93,9 +96,7 @@ public partial class ModCollection public ModCollection Duplicate(string name, LocalCollectionId localId, int index) { Debug.Assert(index > 0, "Collection duplicated with non-positive index."); - return new ModCollection(ModCollectionIdentity.New(name, localId, index), 0, CurrentVersion, - Settings.Select(s => s?.DeepCopy()).ToList(), [.. DirectlyInheritsFrom], - UnusedSettings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.DeepCopy())); + return new ModCollection(ModCollectionIdentity.New(name, localId, index), 0, CurrentVersion, Settings.Clone(), Inheritance.Clone()); } /// Constructor for reading from files. @@ -103,11 +104,8 @@ public partial class ModCollection Dictionary allSettings, IReadOnlyList inheritances) { Debug.Assert(identity.Index > 0, "Collection read with non-positive index."); - var ret = new ModCollection(identity, 0, version, [], [], allSettings) - { - InheritanceByName = inheritances, - }; - ret.ApplyModSettings(saver, mods); + var ret = new ModCollection(identity, 0, version, new ModSettingProvider(allSettings), new ModCollectionInheritance(inheritances)); + ret.Settings.ApplyModSettings(ret, saver, mods); ModCollectionMigration.Migrate(saver, mods, version, ret); return ret; } @@ -116,7 +114,8 @@ public partial class ModCollection public static ModCollection CreateTemporary(string name, LocalCollectionId localId, int index, int changeCounter) { Debug.Assert(index < 0, "Temporary collection created with non-negative index."); - var ret = new ModCollection(ModCollectionIdentity.New(name, localId, index), changeCounter, CurrentVersion, [], [], []); + var ret = new ModCollection(ModCollectionIdentity.New(name, localId, index), changeCounter, CurrentVersion, new ModSettingProvider(), + new ModCollectionInheritance()); return ret; } @@ -124,64 +123,18 @@ public partial class ModCollection public static ModCollection CreateEmpty(string name, LocalCollectionId localId, int index, int modCount) { Debug.Assert(index >= 0, "Empty collection created with negative index."); - return new ModCollection(ModCollectionIdentity.New(name, localId, index), 0, CurrentVersion, - Enumerable.Repeat((ModSettings?)null, modCount).ToList(), [], []); + return new ModCollection(ModCollectionIdentity.New(name, localId, index), 0, CurrentVersion, ModSettingProvider.Empty(modCount), + new ModCollectionInheritance()); } - /// Add settings for a new appended mod, by checking if the mod had settings from a previous deletion. - internal bool AddMod(Mod mod) + private ModCollection(ModCollectionIdentity identity, int changeCounter, int version, ModSettingProvider settings, + ModCollectionInheritance inheritance) { - if (UnusedSettings.TryGetValue(mod.ModPath.Name, out var save)) - { - var ret = save.ToSettings(mod, out var settings); - ((List)Settings).Add(settings); - ((Dictionary)UnusedSettings).Remove(mod.ModPath.Name); - return ret; - } - - ((List)Settings).Add(null); - return false; - } - - /// Move settings from the current mod list to the unused mod settings. - internal void RemoveMod(Mod mod) - { - var settings = Settings[mod.Index]; - if (settings != null) - ((Dictionary)UnusedSettings)[mod.ModPath.Name] = new ModSettings.SavedSettings(settings, mod); - - ((List)Settings).RemoveAt(mod.Index); - } - - /// Move all settings to unused settings for rediscovery. - internal void PrepareModDiscovery(ModStorage mods) - { - foreach (var (mod, setting) in mods.Zip(Settings).Where(s => s.Second != null)) - ((Dictionary)UnusedSettings)[mod.ModPath.Name] = new ModSettings.SavedSettings(setting!, mod); - - ((List)Settings).Clear(); - } - - /// - /// Apply all mod settings from unused settings to the current set of mods. - /// Also fixes invalid settings. - /// - internal void ApplyModSettings(SaveService saver, ModStorage mods) - { - ((List)Settings).Capacity = Math.Max(((List)Settings).Capacity, mods.Count); - if (mods.Aggregate(false, (current, mod) => current | AddMod(mod))) - saver.ImmediateSave(new ModCollectionSave(mods, this)); - } - - private ModCollection(ModCollectionIdentity identity, int changeCounter, int version, List appliedSettings, - List inheritsFrom, Dictionary settings) - { - Identity = identity; - Counters = new CollectionCounters(changeCounter); - Settings = appliedSettings; - UnusedSettings = settings; - DirectlyInheritsFrom = inheritsFrom; - foreach (var c in DirectlyInheritsFrom) - ((List)c.DirectParentOf).Add(this); + Identity = identity; + Counters = new CollectionCounters(changeCounter); + Settings = settings; + Inheritance = inheritance; + ModCollectionInheritance.UpdateChildren(this); + ModCollectionInheritance.UpdateFlattenedInheritance(this); } } diff --git a/Penumbra/Collections/ModCollectionInheritance.cs b/Penumbra/Collections/ModCollectionInheritance.cs new file mode 100644 index 00000000..151ed7db --- /dev/null +++ b/Penumbra/Collections/ModCollectionInheritance.cs @@ -0,0 +1,92 @@ +using OtterGui.Filesystem; + +namespace Penumbra.Collections; + +public struct ModCollectionInheritance +{ + public IReadOnlyList? InheritanceByName { get; private set; } + private readonly List _directlyInheritsFrom = []; + private readonly List _directlyInheritedBy = []; + private readonly List _flatHierarchy = []; + + public ModCollectionInheritance() + { } + + private ModCollectionInheritance(List inheritsFrom) + => _directlyInheritsFrom = [.. inheritsFrom]; + + public ModCollectionInheritance(IReadOnlyList byName) + => InheritanceByName = byName; + + public ModCollectionInheritance Clone() + => new(_directlyInheritsFrom); + + public IEnumerable Identifiers + => InheritanceByName ?? _directlyInheritsFrom.Select(c => c.Identity.Identifier); + + public IReadOnlyList? ConsumeNames() + { + var ret = InheritanceByName; + InheritanceByName = null; + return ret; + } + + public static void UpdateChildren(ModCollection parent) + { + foreach (var inheritance in parent.Inheritance.DirectlyInheritsFrom) + inheritance.Inheritance._directlyInheritedBy.Add(parent); + } + + public void AddInheritance(ModCollection inheritor, ModCollection newParent) + { + _directlyInheritsFrom.Add(newParent); + newParent.Inheritance._directlyInheritedBy.Add(inheritor); + UpdateFlattenedInheritance(inheritor); + } + + public ModCollection RemoveInheritanceAt(ModCollection inheritor, int idx) + { + var parent = DirectlyInheritsFrom[idx]; + _directlyInheritsFrom.RemoveAt(idx); + parent.Inheritance._directlyInheritedBy.Remove(parent); + UpdateFlattenedInheritance(inheritor); + return parent; + } + + public bool MoveInheritance(ModCollection inheritor, int from, int to) + { + if (!_directlyInheritsFrom.Move(from, to)) + return false; + + UpdateFlattenedInheritance(inheritor); + return true; + } + + public void RemoveChild(ModCollection child) + => _directlyInheritedBy.Remove(child); + + /// Contains all direct parent collections this collection inherits settings from. + public readonly IReadOnlyList DirectlyInheritsFrom + => _directlyInheritsFrom; + + /// Contains all direct child collections that inherit from this collection. + public readonly IReadOnlyList DirectlyInheritedBy + => _directlyInheritedBy; + + /// + /// Iterate over all collections inherited from in depth-first order. + /// Skip already visited collections to avoid circular dependencies. + /// + public readonly IReadOnlyList FlatHierarchy + => _flatHierarchy; + + public static void UpdateFlattenedInheritance(ModCollection parent) + { + parent.Inheritance._flatHierarchy.Clear(); + parent.Inheritance._flatHierarchy.AddRange(InheritedCollections(parent).Distinct()); + } + + /// All inherited collections in application order without filtering for duplicates. + private static IEnumerable InheritedCollections(ModCollection parent) + => parent.Inheritance.DirectlyInheritsFrom.SelectMany(InheritedCollections).Prepend(parent); +} diff --git a/Penumbra/Collections/ModCollectionSave.cs b/Penumbra/Collections/ModCollectionSave.cs index 6e1b51ac..4c41a28c 100644 --- a/Penumbra/Collections/ModCollectionSave.cs +++ b/Penumbra/Collections/ModCollectionSave.cs @@ -32,19 +32,19 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection j.WriteValue(modCollection.Identity.Identifier); j.WritePropertyName(nameof(ModCollectionIdentity.Name)); j.WriteValue(modCollection.Identity.Name); - j.WritePropertyName(nameof(ModCollection.Settings)); + j.WritePropertyName("Settings"); // Write all used and unused settings by mod directory name. j.WriteStartObject(); - var list = new List<(string, ModSettings.SavedSettings)>(modCollection.Settings.Count + modCollection.UnusedSettings.Count); + var list = new List<(string, ModSettings.SavedSettings)>(modCollection.Settings.Count + modCollection.Settings.Unused.Count); for (var i = 0; i < modCollection.Settings.Count; ++i) { - var settings = modCollection.Settings[i]; + var settings = modCollection.GetOwnSettings(i); if (settings != null) list.Add((modStorage[i].ModPath.Name, new ModSettings.SavedSettings(settings, modStorage[i]))); } - list.AddRange(modCollection.UnusedSettings.Select(kvp => (kvp.Key, kvp.Value))); + list.AddRange(modCollection.Settings.Unused.Select(kvp => (kvp.Key, kvp.Value))); list.Sort((a, b) => string.Compare(a.Item1, b.Item1, StringComparison.OrdinalIgnoreCase)); foreach (var (modDir, settings) in list) @@ -57,7 +57,7 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection // Inherit by collection name. j.WritePropertyName("Inheritance"); - x.Serialize(j, modCollection.InheritanceByName ?? modCollection.DirectlyInheritsFrom.Select(c => c.Identity.Identifier)); + x.Serialize(j, modCollection.Inheritance.Identifiers); j.WriteEndObject(); } @@ -82,7 +82,7 @@ internal readonly struct ModCollectionSave(ModStorage modStorage, ModCollection name = obj[nameof(ModCollectionIdentity.Name)]?.ToObject() ?? string.Empty; id = obj[nameof(ModCollectionIdentity.Id)]?.ToObject() ?? (version == 1 ? Guid.NewGuid() : Guid.Empty); // Custom deserialization that is converted with the constructor. - settings = obj[nameof(ModCollection.Settings)]?.ToObject>() ?? settings; + settings = obj["Settings"]?.ToObject>() ?? settings; inheritance = obj["Inheritance"]?.ToObject>() ?? inheritance; return true; } diff --git a/Penumbra/Collections/ModSettingProvider.cs b/Penumbra/Collections/ModSettingProvider.cs new file mode 100644 index 00000000..3bf2f949 --- /dev/null +++ b/Penumbra/Collections/ModSettingProvider.cs @@ -0,0 +1,98 @@ +using Penumbra.Mods; +using Penumbra.Mods.Manager; +using Penumbra.Mods.Settings; +using Penumbra.Services; + +namespace Penumbra.Collections; + +public readonly struct ModSettingProvider +{ + private ModSettingProvider(IEnumerable settings, Dictionary unusedSettings) + { + _settings = settings.Select(s => s.DeepCopy()).ToList(); + _unused = unusedSettings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.DeepCopy()); + } + + public ModSettingProvider() + { } + + public static ModSettingProvider Empty(int count) + => new(Enumerable.Repeat(FullModSettings.Empty, count), []); + + public ModSettingProvider(Dictionary allSettings) + => _unused = allSettings; + + private readonly List _settings = []; + + /// Settings for deleted mods will be kept via the mods identifier (directory name). + private readonly Dictionary _unused = []; + + public int Count + => _settings.Count; + + public bool RemoveUnused(string key) + => _unused.Remove(key); + + internal void Set(Index index, ModSettings? settings) + => _settings[index] = _settings[index] with { Settings = settings }; + + internal void SetTemporary(Index index, TemporaryModSettings? settings) + => _settings[index] = _settings[index] with { TempSettings = settings }; + + internal void SetAll(Index index, FullModSettings settings) + => _settings[index] = settings; + + public IReadOnlyList Settings + => _settings; + + public IReadOnlyDictionary Unused + => _unused; + + public ModSettingProvider Clone() + => new(_settings, _unused); + + /// Add settings for a new appended mod, by checking if the mod had settings from a previous deletion. + internal bool AddMod(Mod mod) + { + if (_unused.Remove(mod.ModPath.Name, out var save)) + { + var ret = save.ToSettings(mod, out var settings); + _settings.Add(new FullModSettings(settings)); + return ret; + } + + _settings.Add(FullModSettings.Empty); + return false; + } + + /// Move settings from the current mod list to the unused mod settings. + internal void RemoveMod(Mod mod) + { + var settings = _settings[mod.Index]; + if (settings.Settings != null) + _unused[mod.ModPath.Name] = new ModSettings.SavedSettings(settings.Settings, mod); + + _settings.RemoveAt(mod.Index); + } + + /// Move all settings to unused settings for rediscovery. + internal void PrepareModDiscovery(ModStorage mods) + { + foreach (var (mod, setting) in mods.Zip(_settings).Where(s => s.Second.Settings != null)) + _unused[mod.ModPath.Name] = new ModSettings.SavedSettings(setting.Settings!, mod); + + _settings.Clear(); + } + + /// + /// Apply all mod settings from unused settings to the current set of mods. + /// Also fixes invalid settings. + /// + internal void ApplyModSettings(ModCollection parent, SaveService saver, ModStorage mods) + { + _settings.Capacity = Math.Max(_settings.Capacity, mods.Count); + var settings = this; + if (mods.Aggregate(false, (current, mod) => current | settings.AddMod(mod))) + saver.ImmediateSave(new ModCollectionSave(mods, parent)); + } +} diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 61946978..dee46e32 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -606,7 +606,7 @@ public class CommandHandler : IDisposable, IApiService private bool HandleModState(int settingState, ModCollection collection, Mod mod) { - var settings = collection.Settings[mod.Index]; + var settings = collection.GetOwnSettings(mod.Index); switch (settingState) { case 0: diff --git a/Penumbra/Interop/ResourceTree/ResourceNode.cs b/Penumbra/Interop/ResourceTree/ResourceNode.cs index 088527ca..4fa13e1f 100644 --- a/Penumbra/Interop/ResourceTree/ResourceNode.cs +++ b/Penumbra/Interop/ResourceTree/ResourceNode.cs @@ -98,6 +98,6 @@ public class ResourceNode : ICloneable public readonly record struct UiData(string? Name, ChangedItemIconFlag IconFlag) { public UiData PrependName(string prefix) - => Name == null ? this : new UiData(prefix + Name, IconFlag); + => Name == null ? this : this with { Name = prefix + Name }; } } diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index 876fe12f..c06af9c7 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -74,6 +74,9 @@ public class ModMetaEditor( dict.ClearForDefault(); var count = 0; + foreach (var value in clone.GlobalEqp) + dict.TryAdd(value); + foreach (var (key, value) in clone.Imc) { var defaultEntry = ImcChecker.GetDefaultEntry(key, false); diff --git a/Penumbra/Mods/ModSelection.cs b/Penumbra/Mods/ModSelection.cs index 73d0272b..59cd5d71 100644 --- a/Penumbra/Mods/ModSelection.cs +++ b/Penumbra/Mods/ModSelection.cs @@ -36,9 +36,16 @@ public class ModSelection : EventWrapper _communicator.ModSettingChanged.Subscribe(OnSettingChange, ModSettingChanged.Priority.ModSelection); } - public ModSettings Settings { get; private set; } = ModSettings.Empty; - public ModCollection Collection { get; private set; } = ModCollection.Empty; - public Mod? Mod { get; private set; } + public ModSettings Settings { get; private set; } = ModSettings.Empty; + public ModCollection Collection { get; private set; } = ModCollection.Empty; + public Mod? Mod { get; private set; } + public ModSettings? OwnSettings { get; private set; } + + public bool IsTemporary + => OwnSettings != Settings; + + public TemporaryModSettings? AsTemporarySettings + => Settings as TemporaryModSettings; public void SelectMod(Mod? mod) @@ -83,12 +90,14 @@ public class ModSelection : EventWrapper { if (Mod == null) { - Settings = ModSettings.Empty; - Collection = ModCollection.Empty; + Settings = ModSettings.Empty; + Collection = ModCollection.Empty; + OwnSettings = null; } else { - (var settings, Collection) = _collections.Current[Mod.Index]; + (var settings, Collection) = _collections.Current.GetActualSettings(Mod.Index); + OwnSettings = _collections.Current.GetOwnSettings(Mod.Index); Settings = settings ?? ModSettings.Empty; } } diff --git a/Penumbra/Mods/Settings/ModSettings.cs b/Penumbra/Mods/Settings/ModSettings.cs index 25e4805d..671fba4d 100644 --- a/Penumbra/Mods/Settings/ModSettings.cs +++ b/Penumbra/Mods/Settings/ModSettings.cs @@ -12,7 +12,7 @@ namespace Penumbra.Mods.Settings; public class ModSettings { public static readonly ModSettings Empty = new(); - public SettingList Settings { get; private init; } = []; + public SettingList Settings { get; internal init; } = []; public ModPriority Priority { get; set; } public bool Enabled { get; set; } diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index a2594145..69dfe3e8 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -245,7 +245,7 @@ public class Penumbra : IDalamudPlugin void PrintCollection(ModCollection c, CollectionCache _) => sb.Append( - $"> **`Collection {c.Identity.AnonymizedName + ':',-18}`** Inheritances: `{c.DirectlyInheritsFrom.Count,3}`, Enabled Mods: `{c.ActualSettings.Count(s => s is { Enabled: true }),4}`, Conflicts: `{c.AllConflicts.SelectMany(x => x).Sum(x => x is { HasPriority: true, Solved: true } ? x.Conflicts.Count : 0),5}/{c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority ? x.Conflicts.Count : 0),5}`\n"); + $"> **`Collection {c.Identity.AnonymizedName + ':',-18}`** Inheritances: `{c.Inheritance.DirectlyInheritsFrom.Count,3}`, Enabled Mods: `{c.ActualSettings.Count(s => s is { Enabled: true }),4}`, Conflicts: `{c.AllConflicts.SelectMany(x => x).Sum(x => x is { HasPriority: true, Solved: true } ? x.Conflicts.Count : 0),5}/{c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority ? x.Conflicts.Count : 0),5}`\n"); sb.AppendLine("**Collections**"); sb.Append($"> **`#Collections: `** {_collectionManager.Storage.Count - 1}\n"); diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index f58eb891..9fe8c420 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -240,7 +240,7 @@ public class ConfigMigrationService(SaveService saveService, BackupService backu if (jObject["Name"]?.ToObject() == ForcedCollection) continue; - jObject[nameof(ModCollection.DirectlyInheritsFrom)] = JToken.FromObject(new List { ForcedCollection }); + jObject[nameof(ModCollectionInheritance.DirectlyInheritsFrom)] = JToken.FromObject(new List { ForcedCollection }); File.WriteAllText(collection.FullName, jObject.ToString()); } catch (Exception e) diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 3f7f2f6c..b0029f08 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -737,7 +737,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService if (collectionType is not CollectionType.Current || _mod == null || newCollection == null) return; - UpdateMod(_mod, _mod.Index < newCollection.Settings.Count ? newCollection[_mod.Index].Settings : null); + UpdateMod(_mod, _mod.Index < newCollection.Settings.Count ? newCollection.GetInheritedSettings(_mod.Index).Settings : null); } private void OnSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting oldValue, int groupIdx, bool inherited) @@ -754,7 +754,7 @@ public class ItemSwapTab : IDisposable, ITab, IUiService if (collection != _collectionManager.Active.Current || _mod == null) return; - UpdateMod(_mod, collection[_mod.Index].Settings); + UpdateMod(_mod, collection.GetInheritedSettings(_mod.Index).Settings); _swapData.LoadMod(_mod, _modSettings); _dirty = true; } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 1a4065bb..02e945f3 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -101,7 +101,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService _modelTab.Reset(); _materialTab.Reset(); _shaderPackageTab.Reset(); - _itemSwapTab.UpdateMod(mod, _activeCollections.Current[mod.Index].Settings); + _itemSwapTab.UpdateMod(mod, _activeCollections.Current.GetInheritedSettings(mod.Index).Settings); UpdateModels(); _forceTextureStartPath = true; }); diff --git a/Penumbra/UI/CollectionTab/CollectionPanel.cs b/Penumbra/UI/CollectionTab/CollectionPanel.cs index cab34b10..8b41b105 100644 --- a/Penumbra/UI/CollectionTab/CollectionPanel.cs +++ b/Penumbra/UI/CollectionTab/CollectionPanel.cs @@ -15,6 +15,7 @@ using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.GameData.Enums; using Penumbra.Mods.Manager; +using Penumbra.Mods.Settings; using Penumbra.Services; using Penumbra.UI.Classes; @@ -497,7 +498,7 @@ public sealed class CollectionPanel( ImGui.Separator(); var buttonHeight = 2 * ImGui.GetTextLineHeightWithSpacing(); - if (_inUseCache.Count == 0 && collection.DirectParentOf.Count == 0) + if (_inUseCache.Count == 0 && collection.Inheritance.DirectlyInheritedBy.Count == 0) { ImGui.Dummy(Vector2.One); using var f = _nameFont.Push(); @@ -559,7 +560,7 @@ public sealed class CollectionPanel( private void DrawInheritanceStatistics(ModCollection collection, Vector2 buttonWidth) { - if (collection.DirectParentOf.Count <= 0) + if (collection.Inheritance.DirectlyInheritedBy.Count <= 0) return; using (var _ = ImRaii.PushStyle(ImGuiStyleVar.FramePadding, Vector2.Zero)) @@ -570,11 +571,11 @@ public sealed class CollectionPanel( using var f = _nameFont.Push(); using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); using var color = ImRaii.PushColor(ImGuiCol.Border, Colors.MetaInfoText); - ImGuiUtil.DrawTextButton(Name(collection.DirectParentOf[0]), Vector2.Zero, 0); + ImGuiUtil.DrawTextButton(Name(collection.Inheritance.DirectlyInheritedBy[0]), Vector2.Zero, 0); var constOffset = (ImGui.GetStyle().FramePadding.X + ImGuiHelpers.GlobalScale) * 2 + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X; - foreach (var parent in collection.DirectParentOf.Skip(1)) + foreach (var parent in collection.Inheritance.DirectlyInheritedBy.Skip(1)) { var name = Name(parent); var size = ImGui.CalcTextSize(name).X; @@ -602,7 +603,7 @@ public sealed class CollectionPanel( ImGui.TableSetupColumn("State", ImGuiTableColumnFlags.WidthFixed, 1.75f * ImGui.GetFrameHeight()); ImGui.TableSetupColumn("Priority", ImGuiTableColumnFlags.WidthFixed, 2.5f * ImGui.GetFrameHeight()); ImGui.TableHeadersRow(); - foreach (var (mod, (settings, parent)) in mods.Select(m => (m, collection[m.Index])) + foreach (var (mod, (settings, parent)) in mods.Select(m => (m, collection.GetInheritedSettings(m.Index))) .Where(t => t.Item2.Settings != null) .OrderBy(t => t.m.Name)) { @@ -625,12 +626,12 @@ public sealed class CollectionPanel( private void DrawInactiveSettingsList(ModCollection collection) { - if (collection.UnusedSettings.Count == 0) + if (collection.Settings.Unused.Count == 0) return; ImGui.Dummy(Vector2.One); - var text = collection.UnusedSettings.Count > 1 - ? $"Clear all {collection.UnusedSettings.Count} unused settings from deleted mods." + var text = collection.Settings.Unused.Count > 1 + ? $"Clear all {collection.Settings.Unused.Count} unused settings from deleted mods." : "Clear the currently unused setting from a deleted mods."; if (ImGui.Button(text, new Vector2(ImGui.GetContentRegionAvail().X, 0))) _collections.CleanUnavailableSettings(collection); @@ -638,7 +639,7 @@ public sealed class CollectionPanel( ImGui.Dummy(Vector2.One); var size = new Vector2(ImGui.GetContentRegionAvail().X, - Math.Min(10, collection.UnusedSettings.Count + 1) * ImGui.GetFrameHeightWithSpacing()); + Math.Min(10, collection.Settings.Unused.Count + 1) * ImGui.GetFrameHeightWithSpacing()); using var table = ImRaii.Table("##inactiveSettings", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, size); if (!table) return; @@ -650,7 +651,7 @@ public sealed class CollectionPanel( ImGui.TableSetupColumn("Priority", ImGuiTableColumnFlags.WidthFixed, 2.5f * ImGui.GetFrameHeight()); ImGui.TableHeadersRow(); string? delete = null; - foreach (var (name, settings) in collection.UnusedSettings.OrderBy(n => n.Key)) + foreach (var (name, settings) in collection.Settings.Unused.OrderBy(n => n.Key)) { using var id = ImRaii.PushId(name); ImGui.TableNextColumn(); diff --git a/Penumbra/UI/CollectionTab/InheritanceUi.cs b/Penumbra/UI/CollectionTab/InheritanceUi.cs index a4d60b13..ce3cc3cb 100644 --- a/Penumbra/UI/CollectionTab/InheritanceUi.cs +++ b/Penumbra/UI/CollectionTab/InheritanceUi.cs @@ -107,7 +107,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService var lineEnd = lineStart; // Skip the collection itself. - foreach (var inheritance in collection.GetFlattenedInheritance().Skip(1)) + foreach (var inheritance in collection.Inheritance.FlatHierarchy.Skip(1)) { // Draw the child, already seen collections are colored as conflicts. using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.HandledConflictMod.Value(), @@ -150,7 +150,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService DrawInheritedChildren(collection); else // We still want to keep track of conflicts. - _seenInheritedCollections.UnionWith(collection.GetFlattenedInheritance()); + _seenInheritedCollections.UnionWith(collection.Inheritance.FlatHierarchy); } /// Draw the list box containing the current inheritance information. @@ -163,7 +163,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService _seenInheritedCollections.Clear(); _seenInheritedCollections.Add(_active.Current); - foreach (var collection in _active.Current.DirectlyInheritsFrom.ToList()) + foreach (var collection in _active.Current.Inheritance.DirectlyInheritsFrom.ToList()) DrawInheritance(collection); } @@ -180,7 +180,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService using var target = ImRaii.DragDropTarget(); if (target.Success && ImGuiUtil.IsDropping(InheritanceDragDropLabel)) - _inheritanceAction = (_active.Current.DirectlyInheritsFrom.IndexOf(_movedInheritance!), -1); + _inheritanceAction = (_active.Current.Inheritance.DirectlyInheritsFrom.IndexOf(_movedInheritance!), -1); } /// @@ -244,7 +244,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService { ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton); _newInheritance ??= _collections.FirstOrDefault(c - => c != _active.Current && !_active.Current.DirectlyInheritsFrom.Contains(c)) + => c != _active.Current && !_active.Current.Inheritance.DirectlyInheritsFrom.Contains(c)) ?? ModCollection.Empty; using var combo = ImRaii.Combo("##newInheritance", Name(_newInheritance)); if (!combo) @@ -271,8 +271,8 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService if (_movedInheritance != null) { - var idx1 = _active.Current.DirectlyInheritsFrom.IndexOf(_movedInheritance); - var idx2 = _active.Current.DirectlyInheritsFrom.IndexOf(collection); + var idx1 = _active.Current.Inheritance.DirectlyInheritsFrom.IndexOf(_movedInheritance); + var idx2 = _active.Current.Inheritance.DirectlyInheritsFrom.IndexOf(collection); if (idx1 >= 0 && idx2 >= 0) _inheritanceAction = (idx1, idx2); } @@ -302,7 +302,7 @@ public class InheritanceUi(CollectionManager collectionManager, IncognitoService if (ImGui.GetIO().KeyCtrl && ImGui.IsItemClicked(ImGuiMouseButton.Right)) { if (withDelete && ImGui.GetIO().KeyShift) - _inheritanceAction = (_active.Current.DirectlyInheritsFrom.IndexOf(collection), -1); + _inheritanceAction = (_active.Current.Inheritance.DirectlyInheritsFrom.IndexOf(collection), -1); else _newCurrentCollection = collection; } diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 0781312c..4607434c 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -201,7 +201,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector 0) { @@ -650,14 +664,14 @@ public sealed class ModFileSystemSelector : FileSystemSelector kvp.Key)) { ImUtf8.DrawTableColumn($"{id:D6}"); ImUtf8.DrawTableColumn(name.Span); } - } } } } diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index c226098d..8b4913c8 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -82,7 +82,7 @@ public class ModsTab( + $"{selector.SortMode.Name} Sort Mode\n" + $"{selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n" + $"{selector.Selected?.Name ?? "NULL"} Selected Mod\n" - + $"{string.Join(", ", _activeCollections.Current.DirectlyInheritsFrom.Select(c => c.Identity.AnonymizedName))} Inheritances\n"); + + $"{string.Join(", ", _activeCollections.Current.Inheritance.DirectlyInheritsFrom.Select(c => c.Identity.AnonymizedName))} Inheritances\n"); } }