diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs index 2604a49d..5ed26ce5 100644 --- a/Penumbra/Api/Api/ModSettingsApi.cs +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -56,13 +56,13 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable var dict = new Dictionary(mod.Groups.Count); foreach (var g in mod.Groups) - dict.Add(g.Name, (g.Select(o => o.Name).ToArray(), (int)g.Type)); + dict.Add(g.Name, (g.Options.Select(o => o.Name).ToArray(), (int)g.Type)); 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.Select(o => o.Name).ToArray(), (int)g.Type)) + ? 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, @@ -153,7 +153,7 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable if (groupIdx < 0) return ApiHelpers.Return(PenumbraApiEc.OptionGroupMissing, args); - var optionIdx = mod.Groups[groupIdx].IndexOf(o => o.Name == optionName); + var optionIdx = mod.Groups[groupIdx].Options.IndexOf(o => o.Name == optionName); if (optionIdx < 0) return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); @@ -190,7 +190,7 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable { case SingleModGroup single: { - var optionIdx = optionNames.Count == 0 ? -1 : single.IndexOf(o => o.Name == optionNames[^1]); + var optionIdx = optionNames.Count == 0 ? -1 : single.OptionData.IndexOf(o => o.Name == optionNames[^1]); if (optionIdx < 0) return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); @@ -201,7 +201,7 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable { foreach (var name in optionNames) { - var optionIdx = multi.IndexOf(o => o.Name == name); + var optionIdx = multi.PrioritizedOptions.IndexOf(o => o.Mod.Name == name); if (optionIdx < 0) return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index f4b7d47e..7d9388a9 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -203,7 +203,7 @@ public partial class TexToolsImporter { var option = group.OptionList[idx]; _currentOptionName = option.Name; - options.Insert(idx, ModCreator.CreateEmptySubMod(option.Name)); + options.Insert(idx, SubMod.CreateForSaving(option.Name)); if (option.IsChecked) defaultSettings = Setting.Single(idx); } diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index 5283f77e..b1823bd7 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -54,7 +54,13 @@ public unsafe class MetaFileManager if (!dir.Exists) dir.Create(); - foreach (var option in group.OfType()) + var optionEnumerator = group switch + { + SingleModGroup single => single.OptionData, + MultiModGroup multi => multi.PrioritizedOptions.Select(o => o.Mod), + _ => [], + }; + foreach (var option in optionEnumerator) { var optionDir = ModCreator.NewOptionDirectory(dir, option.Name, Config.ReplaceNonAsciiOnImport); if (!optionDir.Exists) diff --git a/Penumbra/Mods/Editor/ModEditor.cs b/Penumbra/Mods/Editor/ModEditor.cs index d9781c06..0a96e0fd 100644 --- a/Penumbra/Mods/Editor/ModEditor.cs +++ b/Penumbra/Mods/Editor/ModEditor.cs @@ -1,3 +1,4 @@ +using System; using OtterGui; using OtterGui.Compression; using Penumbra.Mods.Subclasses; @@ -72,12 +73,18 @@ public class ModEditor( if (groupIdx >= 0) { Group = Mod.Groups[groupIdx]; - if (optionIdx >= 0 && optionIdx < Group.Count) + switch(Group) { - Option = Group[optionIdx]; - GroupIdx = groupIdx; - OptionIdx = optionIdx; - return; + case SingleModGroup single when optionIdx >= 0 && optionIdx < single.OptionData.Count: + Option = single.OptionData[optionIdx]; + GroupIdx = groupIdx; + OptionIdx = optionIdx; + return; + case MultiModGroup multi when optionIdx >= 0 && optionIdx < multi.PrioritizedOptions.Count: + Option = multi.PrioritizedOptions[optionIdx].Mod; + GroupIdx = groupIdx; + OptionIdx = optionIdx; + return; } } } @@ -109,8 +116,17 @@ public class ModEditor( action(mod.Default, -1, 0); foreach (var (group, groupIdx) in mod.Groups.WithIndex()) { - for (var optionIdx = 0; optionIdx < group.Count; ++optionIdx) - action(group[optionIdx], groupIdx, optionIdx); + switch (group) + { + case SingleModGroup single: + for (var optionIdx = 0; optionIdx < single.OptionData.Count; ++optionIdx) + action(single.OptionData[optionIdx], groupIdx, optionIdx); + break; + case MultiModGroup multi: + for (var optionIdx = 0; optionIdx < multi.PrioritizedOptions.Count; ++optionIdx) + action(multi.PrioritizedOptions[optionIdx].Mod, groupIdx, optionIdx); + break; + } } } diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index 4bdf4b1b..51615b05 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -24,7 +24,8 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu num += dict.TryAdd(path.Item2, file.File) ? 0 : 1; } - modManager.OptionEditor.OptionSetFiles(mod, option.GroupIdx, option.OptionIdx, dict); + var (groupIdx, optionIdx) = option.GetIndices(); + modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, dict); files.UpdatePaths(mod, option); Changes = false; return num; diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 25590c49..74e9007c 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -1,5 +1,6 @@ using Dalamud.Interface.Internal.Notifications; using Dalamud.Utility; +using ImGuizmoNET; using OtterGui; using OtterGui.Classes; using Penumbra.Api.Enums; @@ -104,9 +105,16 @@ public class ModMerger : IDisposable ((List)Warnings).Add( $"The merged group {group.Name} already existed, but has a different type {group.Type} than the original group of type {originalGroup.Type}."); - foreach (var originalOption in originalGroup) + var optionEnumerator = group switch { - var (option, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, originalOption.Name); + SingleModGroup single => single.OptionData, + MultiModGroup multi => multi.PrioritizedOptions.Select(o => o.Mod), + _ => [], + }; + + foreach (var originalOption in optionEnumerator) + { + var (option, _, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, originalOption.Name); if (optionCreated) { _createdOptions.Add(option); @@ -138,7 +146,7 @@ public class ModMerger : IDisposable var (group, groupIdx, groupCreated) = _editor.FindOrAddModGroup(MergeToMod!, GroupType.Multi, groupName, SaveType.None); if (groupCreated) _createdGroups.Add(groupIdx); - var (option, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, optionName, SaveType.None); + var (option, _, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, optionName, SaveType.None); if (optionCreated) _createdOptions.Add(option); var dir = ModCreator.NewOptionDirectory(MergeToMod!.ModPath, groupName, _config.ReplaceNonAsciiOnImport); @@ -184,9 +192,10 @@ public class ModMerger : IDisposable } } - _editor.OptionSetFiles(MergeToMod!, option.GroupIdx, option.OptionIdx, redirections, SaveType.None); - _editor.OptionSetFileSwaps(MergeToMod!, option.GroupIdx, option.OptionIdx, swaps, SaveType.None); - _editor.OptionSetManipulations(MergeToMod!, option.GroupIdx, option.OptionIdx, manips, SaveType.ImmediateSync); + var (groupIdx, optionIdx) = option.GetIndices(); + _editor.OptionSetFiles(MergeToMod!, groupIdx, optionIdx, redirections, SaveType.None); + _editor.OptionSetFileSwaps(MergeToMod!, groupIdx, optionIdx, swaps, SaveType.None); + _editor.OptionSetManipulations(MergeToMod!, groupIdx, optionIdx, manips, SaveType.ImmediateSync); return; bool GetFullPath(FullPath input, out FullPath ret) @@ -251,7 +260,7 @@ public class ModMerger : IDisposable Mod? result = null; try { - dir = _creator.CreateEmptyMod(_mods.BasePath, modName, $"Split off from {mods[0].ParentMod.Name}."); + dir = _creator.CreateEmptyMod(_mods.BasePath, modName, $"Split off from {mods[0].Mod.Name}."); if (dir == null) throw new Exception($"Could not split off mods, unable to create new mod with name {modName}."); @@ -268,7 +277,6 @@ public class ModMerger : IDisposable { foreach (var originalOption in mods) { - var originalGroup = originalOption.ParentMod.Groups[originalOption.GroupIdx]; if (originalOption.IsDefault) { var files = CopySubModFiles(mods[0], dir); @@ -278,13 +286,14 @@ public class ModMerger : IDisposable } else { + var originalGroup = originalOption.Group; var (group, groupIdx, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name); - var (option, _) = _editor.FindOrAddOption(result, groupIdx, originalOption.Name); + var (option, optionIdx, _) = _editor.FindOrAddOption(result, groupIdx, originalOption.Name); var folder = Path.Combine(dir.FullName, group.Name, option.Name); var files = CopySubModFiles(originalOption, new DirectoryInfo(folder)); - _editor.OptionSetFiles(result, groupIdx, option.OptionIdx, files); - _editor.OptionSetFileSwaps(result, groupIdx, option.OptionIdx, originalOption.FileSwapData); - _editor.OptionSetManipulations(result, groupIdx, option.OptionIdx, originalOption.ManipulationData); + _editor.OptionSetFiles(result, groupIdx, optionIdx, files); + _editor.OptionSetFileSwaps(result, groupIdx, optionIdx, originalOption.FileSwapData); + _editor.OptionSetManipulations(result, groupIdx, optionIdx, originalOption.ManipulationData); } } } @@ -309,7 +318,7 @@ public class ModMerger : IDisposable private static Dictionary CopySubModFiles(SubMod option, DirectoryInfo newMod) { var ret = new Dictionary(option.FileData.Count); - var parentPath = ((Mod)option.ParentMod).ModPath.FullName; + var parentPath = ((Mod)option.Mod).ModPath.FullName; foreach (var (path, file) in option.FileData) { var target = Path.GetRelativePath(parentPath, file.FullName); @@ -339,7 +348,8 @@ public class ModMerger : IDisposable { foreach (var option in _createdOptions) { - _editor.DeleteOption(MergeToMod!, option.GroupIdx, option.OptionIdx); + var (groupIdx, optionIdx) = option.GetIndices(); + _editor.DeleteOption(MergeToMod!, groupIdx, optionIdx); Penumbra.Log.Verbose($"[Merger] Removed option {option.FullName}."); } diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index a9a31212..9698fdcb 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -167,28 +167,27 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) // Normalize all other options. foreach (var (group, groupIdx) in Mod.Groups.WithIndex()) { - _redirections[groupIdx + 1].EnsureCapacity(group.Count); - for (var i = _redirections[groupIdx + 1].Count; i < group.Count; ++i) - _redirections[groupIdx + 1].Add([]); - var groupDir = ModCreator.CreateModFolder(directory, group.Name, _config.ReplaceNonAsciiOnImport, true); - foreach (var option in group.OfType()) + switch (group) { - var optionDir = ModCreator.CreateModFolder(groupDir, option.Name, _config.ReplaceNonAsciiOnImport, true); + case SingleModGroup single: + _redirections[groupIdx + 1].EnsureCapacity(single.OptionData.Count); + for (var i = _redirections[groupIdx + 1].Count; i < single.OptionData.Count; ++i) + _redirections[groupIdx + 1].Add([]); - newDict = _redirections[groupIdx + 1][option.OptionIdx]; - newDict.Clear(); - newDict.EnsureCapacity(option.FileData.Count); - foreach (var (gamePath, fullPath) in option.FileData) - { - var relPath = new Utf8RelPath(gamePath).ToString(); - var newFullPath = Path.Combine(optionDir.FullName, relPath); - var redirectPath = new FullPath(Path.Combine(Mod.ModPath.FullName, groupDir.Name, optionDir.Name, relPath)); - Directory.CreateDirectory(Path.GetDirectoryName(newFullPath)!); - File.Copy(fullPath.FullName, newFullPath, true); - newDict.Add(gamePath, redirectPath); - ++Step; - } + foreach (var (option, optionIdx) in single.OptionData.WithIndex()) + HandleSubMod(groupDir, option, _redirections[groupIdx + 1][optionIdx]); + + break; + case MultiModGroup multi: + _redirections[groupIdx + 1].EnsureCapacity(multi.PrioritizedOptions.Count); + for (var i = _redirections[groupIdx + 1].Count; i < multi.PrioritizedOptions.Count; ++i) + _redirections[groupIdx + 1].Add([]); + + foreach (var ((option, _), optionIdx) in multi.PrioritizedOptions.WithIndex()) + HandleSubMod(groupDir, option, _redirections[groupIdx + 1][optionIdx]); + + break; } } @@ -200,6 +199,24 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) } return false; + + void HandleSubMod(DirectoryInfo groupDir, SubMod option, Dictionary newDict) + { + var optionDir = ModCreator.CreateModFolder(groupDir, option.Name, _config.ReplaceNonAsciiOnImport, true); + + newDict.Clear(); + newDict.EnsureCapacity(option.FileData.Count); + foreach (var (gamePath, fullPath) in option.FileData) + { + var relPath = new Utf8RelPath(gamePath).ToString(); + var newFullPath = Path.Combine(optionDir.FullName, relPath); + var redirectPath = new FullPath(Path.Combine(Mod.ModPath.FullName, groupDir.Name, optionDir.Name, relPath)); + Directory.CreateDirectory(Path.GetDirectoryName(newFullPath)!); + File.Copy(fullPath.FullName, newFullPath, true); + newDict.Add(gamePath, redirectPath); + ++Step; + } + } } private bool MoveOldFiles() @@ -274,9 +291,20 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) private void ApplyRedirections() { - foreach (var option in Mod.AllSubMods) - _modManager.OptionEditor.OptionSetFiles(Mod, option.GroupIdx, option.OptionIdx, - _redirections[option.GroupIdx + 1][option.OptionIdx]); + foreach (var (group, groupIdx) in Mod.Groups.WithIndex()) + { + switch (group) + { + case SingleModGroup single: + foreach (var (_, optionIdx) in single.OptionData.WithIndex()) + _modManager.OptionEditor.OptionSetFiles(Mod, groupIdx, optionIdx, _redirections[groupIdx + 1][optionIdx]); + break; + case MultiModGroup multi: + foreach (var (_, optionIdx) in multi.PrioritizedOptions.WithIndex()) + _modManager.OptionEditor.OptionSetFiles(Mod, groupIdx, optionIdx, _redirections[groupIdx + 1][optionIdx]); + break; + } + } ++Step; } diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index 99ad1a4f..df243781 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -2,6 +2,7 @@ using Penumbra.Communication; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Subclasses; using Penumbra.Services; namespace Penumbra.Mods.Manager; @@ -211,7 +212,14 @@ public class ModCacheManager : IDisposable foreach (var group in mod.Groups) { mod.HasOptions |= group.IsOption; - foreach (var s in group) + var optionEnumerator = group switch + { + SingleModGroup single => single.OptionData, + MultiModGroup multi => multi.PrioritizedOptions.Select(o => o.Mod), + _ => [], + }; + + foreach (var s in optionEnumerator) { mod.TotalFileCount += s.Files.Count; mod.TotalSwapCount += s.FileSwaps.Count; diff --git a/Penumbra/Mods/Manager/ModMigration.cs b/Penumbra/Mods/Manager/ModMigration.cs index 295afd7b..9c8ced89 100644 --- a/Penumbra/Mods/Manager/ModMigration.cs +++ b/Penumbra/Mods/Manager/ModMigration.cs @@ -126,7 +126,7 @@ public static partial class ModMigration case GroupType.Multi: var optionPriority = ModPriority.Default; - var newMultiGroup = new MultiModGroup() + var newMultiGroup = new MultiModGroup(mod) { Name = group.GroupName, Priority = priority++, @@ -134,7 +134,7 @@ public static partial class ModMigration }; mod.Groups.Add(newMultiGroup); foreach (var option in group.Options) - newMultiGroup.PrioritizedOptions.Add((SubModFromOption(creator, mod, option, seenMetaFiles), optionPriority++)); + newMultiGroup.PrioritizedOptions.Add((SubModFromOption(creator, mod, newMultiGroup, option, seenMetaFiles), optionPriority++)); break; case GroupType.Single: @@ -144,7 +144,7 @@ public static partial class ModMigration return; } - var newSingleGroup = new SingleModGroup() + var newSingleGroup = new SingleModGroup(mod) { Name = group.GroupName, Priority = priority++, @@ -152,7 +152,7 @@ public static partial class ModMigration }; mod.Groups.Add(newSingleGroup); foreach (var option in group.Options) - newSingleGroup.OptionData.Add(SubModFromOption(creator, mod, option, seenMetaFiles)); + newSingleGroup.OptionData.Add(SubModFromOption(creator, mod, newSingleGroup, option, seenMetaFiles)); break; } @@ -171,9 +171,9 @@ public static partial class ModMigration } } - private static SubMod SubModFromOption(ModCreator creator, Mod mod, OptionV0 option, HashSet seenMetaFiles) + private static SubMod SubModFromOption(ModCreator creator, Mod mod, IModGroup group, OptionV0 option, HashSet seenMetaFiles) { - var subMod = new SubMod(mod) { Name = option.OptionName }; + var subMod = new SubMod(mod, group) { Name = option.OptionName }; AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles); creator.IncorporateMetaChanges(subMod, mod.ModPath, false); return subMod; diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index 9d942574..e78b6209 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -87,12 +87,12 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1; mod.Groups.Add(type == GroupType.Multi - ? new MultiModGroup + ? new MultiModGroup(mod) { Name = newName, Priority = maxPriority, } - : new SingleModGroup + : new SingleModGroup(mod) { Name = newName, Priority = maxPriority, @@ -120,7 +120,6 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS { communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1); mod.Groups.RemoveAt(groupIdx); - UpdateSubModPositions(mod, groupIdx); saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport); communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1); } @@ -131,7 +130,6 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS if (!mod.Groups.Move(groupIdxFrom, groupIdxTo)) return; - UpdateSubModPositions(mod, Math.Min(groupIdxFrom, groupIdxTo)); saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport); communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo); } @@ -156,12 +154,9 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS /// Change the description of the given option. public void ChangeOptionDescription(Mod mod, int groupIdx, int optionIdx, string newDescription) { - var group = mod.Groups[groupIdx]; - var option = group[optionIdx]; - if (option.Description == newDescription) + if (!mod.Groups[groupIdx].ChangeOptionDescription(optionIdx, newDescription)) return; - option.Description = newDescription; saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); } @@ -173,12 +168,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS if (group.Priority == newPriority) return; - var _ = group switch - { - SingleModGroup s => s.Priority = newPriority, - MultiModGroup m => m.Priority = newPriority, - _ => newPriority, - }; + group.Priority = newPriority; saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1); } @@ -188,14 +178,11 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS { switch (mod.Groups[groupIdx]) { - case SingleModGroup: - ChangeGroupPriority(mod, groupIdx, newPriority); - break; - case MultiModGroup m: - if (m.PrioritizedOptions[optionIdx].Priority == newPriority) + case MultiModGroup multi: + if (multi.PrioritizedOptions[optionIdx].Priority == newPriority) return; - m.PrioritizedOptions[optionIdx] = (m.PrioritizedOptions[optionIdx].Mod, newPriority); + multi.PrioritizedOptions[optionIdx] = (multi.PrioritizedOptions[optionIdx].Mod, newPriority); saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1); return; @@ -205,60 +192,63 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS /// Rename the given option. public void RenameOption(Mod mod, int groupIdx, int optionIdx, string newName) { - switch (mod.Groups[groupIdx]) - { - case SingleModGroup s: - if (s.OptionData[optionIdx].Name == newName) - return; - - s.OptionData[optionIdx].Name = newName; - break; - case MultiModGroup m: - var option = m.PrioritizedOptions[optionIdx].Mod; - if (option.Name == newName) - return; - - option.Name = newName; - break; - } + if (!mod.Groups[groupIdx].ChangeOptionName(optionIdx, newName)) + return; saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); } /// Add a new empty option of the given name for the given group. - public void AddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue) + public int AddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue) { - var group = mod.Groups[groupIdx]; - var subMod = new SubMod(mod) { Name = newName }; - subMod.SetPosition(groupIdx, group.Count); - switch (group) - { - case SingleModGroup s: - s.OptionData.Add(subMod); - break; - case MultiModGroup m: - m.PrioritizedOptions.Add((subMod, ModPriority.Default)); - break; - } + var group = mod.Groups[groupIdx]; + var idx = group.AddOption(mod, newName); + if (idx < 0) + return -1; saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1); + return idx; } /// Add a new empty option of the given name for the given group if it does not exist already. - public (SubMod, bool) FindOrAddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue) + public (SubMod, int, bool) FindOrAddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue) { var group = mod.Groups[groupIdx]; - var idx = group.IndexOf(o => o.Name == newName); - if (idx >= 0) - return ((SubMod)group[idx], false); + switch (group) + { + case SingleModGroup single: + { + var idx = single.OptionData.IndexOf(o => o.Name == newName); + if (idx >= 0) + return (single.OptionData[idx], idx, false); - AddOption(mod, groupIdx, newName, saveType); - if (group[^1].Name != newName) - throw new Exception($"Could not create new option with name {newName} in {group.Name}."); + idx = single.AddOption(mod, newName); + if (idx < 0) + throw new Exception($"Could not create new option with name {newName} in {group.Name}."); - return ((SubMod)group[^1], true); + saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1); + return (single.OptionData[^1], single.OptionData.Count - 1, true); + } + case MultiModGroup multi: + { + var idx = multi.PrioritizedOptions.IndexOf(o => o.Mod.Name == newName); + if (idx >= 0) + return (multi.PrioritizedOptions[idx].Mod, idx, false); + + idx = multi.AddOption(mod, newName); + if (idx < 0) + throw new Exception($"Could not create new option with name {newName} in {group.Name}."); + + saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1); + return (multi.PrioritizedOptions[^1].Mod, multi.PrioritizedOptions.Count - 1, true); + } + } + + throw new Exception($"{nameof(FindOrAddOption)} is not supported for mod groups of type {group.GetType()}."); } /// Add an existing option to a given group with default priority. @@ -269,25 +259,28 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS public void AddOption(Mod mod, int groupIdx, SubMod option, ModPriority priority) { var group = mod.Groups[groupIdx]; + int idx; switch (group) { - case MultiModGroup { Count: >= IModGroup.MaxMultiOptions }: + case MultiModGroup { PrioritizedOptions.Count: >= IModGroup.MaxMultiOptions }: Penumbra.Log.Error( $"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, " + $"since only up to {IModGroup.MaxMultiOptions} options are supported in one group."); return; case SingleModGroup s: - option.SetPosition(groupIdx, s.Count); + idx = s.OptionData.Count; s.OptionData.Add(option); break; case MultiModGroup m: - option.SetPosition(groupIdx, m.Count); + idx = m.PrioritizedOptions.Count; m.PrioritizedOptions.Add((option, priority)); break; + default: + return; } saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1); } /// Delete the given option from the given group. @@ -306,7 +299,6 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS break; } - group.UpdatePositions(optionIdx); saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1); } @@ -396,16 +388,6 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS return false; } - /// Update the indices stored in options from a given group on. - private static void UpdateSubModPositions(Mod mod, int fromGroup) - { - foreach (var (group, groupIdx) in mod.Groups.WithIndex().Skip(fromGroup)) - { - foreach (var (o, optionIdx) in group.OfType().WithIndex()) - o.SetPosition(groupIdx, optionIdx); - } - } - /// Get the correct option for the given group and option index. private static SubMod GetSubMod(Mod mod, int groupIdx, int optionIdx) { diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 25f3c510..71f64205 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -38,7 +38,7 @@ public sealed class Mod : IMod internal Mod(DirectoryInfo modPath) { ModPath = modPath; - Default = new SubMod(this); + Default = SubMod.CreateDefault(this); } public override string ToString() @@ -82,7 +82,12 @@ public sealed class Mod : IMod } public IEnumerable AllSubMods - => Groups.SelectMany(o => o).Prepend(Default); + => Groups.SelectMany(o => o switch + { + SingleModGroup single => single.OptionData, + MultiModGroup multi => multi.PrioritizedOptions.Select(s => s.Mod), + _ => [], + }).Prepend(Default); public List FindUnusedFiles() { diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 661dd6fb..4d32f395 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -16,7 +16,11 @@ using Penumbra.String.Classes; namespace Penumbra.Mods; -public partial class ModCreator(SaveService _saveService, Configuration config, ModDataEditor _dataEditor, MetaFileManager _metaFileManager, +public partial class ModCreator( + SaveService _saveService, + Configuration config, + ModDataEditor _dataEditor, + MetaFileManager _metaFileManager, GamePathParser _gamePathParser) { public readonly Configuration Config = config; @@ -106,7 +110,6 @@ public partial class ModCreator(SaveService _saveService, Configuration config, public void LoadDefaultOption(Mod mod) { var defaultFile = _saveService.FileNames.OptionGroupFile(mod, -1, Config.ReplaceNonAsciiOnImport); - mod.Default.SetPosition(-1, 0); try { if (!File.Exists(defaultFile)) @@ -241,27 +244,21 @@ public partial class ModCreator(SaveService _saveService, Configuration config, { case GroupType.Multi: { - var group = new MultiModGroup() - { - Name = name, - Description = desc, - Priority = priority, - DefaultSettings = defaultSettings, - }; + var group = MultiModGroup.CreateForSaving(name); + group.Description = desc; + group.Priority = priority; + group.DefaultSettings = defaultSettings; group.PrioritizedOptions.AddRange(subMods.Select((s, idx) => (s, new ModPriority(idx)))); _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); break; } case GroupType.Single: { - var group = new SingleModGroup() - { - Name = name, - Description = desc, - Priority = priority, - DefaultSettings = defaultSettings, - }; - group.OptionData.AddRange(subMods.OfType()); + var group = SingleModGroup.CreateForSaving(name); + group.Description = desc; + group.Priority = priority; + group.DefaultSettings = defaultSettings; + group.OptionData.AddRange(subMods); _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); break; } @@ -275,11 +272,8 @@ public partial class ModCreator(SaveService _saveService, Configuration config, .Select(f => (Utf8GamePath.FromFile(f, optionFolder, out var gamePath, true), gamePath, new FullPath(f))) .Where(t => t.Item1); - var mod = new SubMod(null!) // Mod is irrelevant here, only used for saving. - { - Name = option.Name, - Description = option.Description, - }; + var mod = SubMod.CreateForSaving(option.Name); + mod.Description = option.Description; foreach (var (_, gamePath, file) in list) mod.FileData.TryAdd(gamePath, file); @@ -287,13 +281,6 @@ public partial class ModCreator(SaveService _saveService, Configuration config, return mod; } - /// Create an empty sub mod for single groups with None options. - internal static SubMod CreateEmptySubMod(string name) - => new SubMod(null!) // Mod is irrelevant here, only used for saving. - { - Name = name, - }; - /// /// Create the default data file from all unused files that were not handled before /// and are used in sub mods. diff --git a/Penumbra/Mods/Subclasses/IModDataContainer.cs b/Penumbra/Mods/Subclasses/IModDataContainer.cs new file mode 100644 index 00000000..d0b444b8 --- /dev/null +++ b/Penumbra/Mods/Subclasses/IModDataContainer.cs @@ -0,0 +1,80 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.Meta.Manipulations; +using Penumbra.String.Classes; + +namespace Penumbra.Mods.Subclasses; + +public interface IModDataContainer +{ + public Dictionary Files { get; set; } + public Dictionary FileSwaps { get; set; } + public HashSet Manipulations { get; set; } + + public void AddDataTo(Dictionary redirections, HashSet manipulations) + { + foreach (var (path, file) in Files) + redirections.TryAdd(path, file); + + foreach (var (path, file) in FileSwaps) + redirections.TryAdd(path, file); + manipulations.UnionWith(Manipulations); + } + + public static void Load(JToken json, IModDataContainer data, DirectoryInfo basePath) + { + data.Files.Clear(); + data.FileSwaps.Clear(); + data.Manipulations.Clear(); + + var files = (JObject?)json[nameof(Files)]; + if (files != null) + foreach (var property in files.Properties()) + { + if (Utf8GamePath.FromString(property.Name, out var p, true)) + data.Files.TryAdd(p, new FullPath(basePath, property.Value.ToObject())); + } + + var swaps = (JObject?)json[nameof(FileSwaps)]; + if (swaps != null) + foreach (var property in swaps.Properties()) + { + if (Utf8GamePath.FromString(property.Name, out var p, true)) + data.FileSwaps.TryAdd(p, new FullPath(property.Value.ToObject()!)); + } + + var manips = json[nameof(Manipulations)]; + if (manips != null) + foreach (var s in manips.Children().Select(c => c.ToObject()) + .Where(m => m.Validate())) + data.Manipulations.Add(s); + } + + public static void WriteModData(JsonWriter j, JsonSerializer serializer, IModDataContainer data, DirectoryInfo basePath) + { + j.WritePropertyName(nameof(data.Files)); + j.WriteStartObject(); + foreach (var (gamePath, file) in data.Files) + { + if (file.ToRelPath(basePath, out var relPath)) + { + j.WritePropertyName(gamePath.ToString()); + j.WriteValue(relPath.ToString()); + } + } + + j.WriteEndObject(); + j.WritePropertyName(nameof(data.FileSwaps)); + j.WriteStartObject(); + foreach (var (gamePath, file) in data.FileSwaps) + { + j.WritePropertyName(gamePath.ToString()); + j.WriteValue(file.ToString()); + } + + j.WriteEndObject(); + j.WritePropertyName(nameof(data.Manipulations)); + serializer.Serialize(j, data.Manipulations); + j.WriteEndObject(); + } +} diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index 96d7c6b7..a046ade0 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -6,25 +6,27 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; -public interface IModGroup : IReadOnlyCollection +public interface IModGroup { public const int MaxMultiOptions = 63; + public Mod Mod { get; } public string Name { get; } public string Description { get; } public GroupType Type { get; } - public ModPriority Priority { get; } + public ModPriority Priority { get; set; } public Setting DefaultSettings { get; set; } public FullPath? FindBestMatch(Utf8GamePath gamePath); + public int AddOption(Mod mod, string name, string description = ""); + public bool ChangeOptionDescription(int optionIndex, string newDescription); + public bool ChangeOptionName(int optionIndex, string newName); - public SubMod this[Index idx] { get; } - - public bool IsOption { get; } + public IReadOnlyList Options { get; } + public bool IsOption { get; } public IModGroup Convert(GroupType type); public bool MoveOption(int optionIdxFrom, int optionIdxTo); - public void UpdatePositions(int from = 0); public void AddData(Setting setting, Dictionary redirections, HashSet manipulations); diff --git a/Penumbra/Mods/Subclasses/IModOption.cs b/Penumbra/Mods/Subclasses/IModOption.cs new file mode 100644 index 00000000..bb52a2cd --- /dev/null +++ b/Penumbra/Mods/Subclasses/IModOption.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Penumbra.Mods.Subclasses; + +public interface IModOption +{ + public string Name { get; set; } + public string FullName { get; } + public string Description { get; set; } + + public static void Load(JToken json, IModOption option) + { + option.Name = json[nameof(Name)]?.ToObject() ?? string.Empty; + option.Description = json[nameof(Description)]?.ToObject() ?? string.Empty; + } + + public static void WriteModOption(JsonWriter j, IModOption option) + { + j.WritePropertyName(nameof(Name)); + j.WriteValue(option.Name); + j.WritePropertyName(nameof(Description)); + j.WriteValue(option.Description); + } +} diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs index 81a3bb41..2ddabdb8 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Subclasses/ModSettings.cs @@ -68,7 +68,7 @@ public class ModSettings var config = Settings[groupIdx]; Settings[groupIdx] = group.Type switch { - GroupType.Single => config.TurnMulti(group.Count), + GroupType.Single => config.TurnMulti(group.Options.Count), GroupType.Multi => Setting.Multi((int)config.Value), _ => config, }; @@ -182,15 +182,15 @@ public class ModSettings if (idx >= mod.Groups.Count) break; - var group = mod.Groups[idx]; - if (group.Type == GroupType.Single && setting.Value < (ulong)group.Count) + switch (mod.Groups[idx]) { - dict.Add(group.Name, [group[(int)setting.Value].Name]); - } - else - { - var list = group.Where((_, optionIdx) => (setting.Value & (1ul << optionIdx)) != 0).Select(o => o.Name).ToList(); - dict.Add(group.Name, list); + case SingleModGroup single when setting.Value < (ulong)single.Options.Count: + dict.Add(single.Name, [single.Options[setting.AsIndex].Name]); + break; + case MultiModGroup multi: + var list = multi.Options.WithIndex().Where(p => setting.HasFlag(p.Index)).Select(p => p.Value.Name).ToList(); + dict.Add(multi.Name, list); + break; } } diff --git a/Penumbra/Mods/Subclasses/MultiModGroup.cs b/Penumbra/Mods/Subclasses/MultiModGroup.cs index 02ae07f4..4ec2c72a 100644 --- a/Penumbra/Mods/Subclasses/MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/MultiModGroup.cs @@ -1,5 +1,4 @@ using Dalamud.Interface.Internal.Notifications; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; @@ -11,11 +10,12 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; /// Groups that allow all available options to be selected at once. -public sealed class MultiModGroup : IModGroup +public sealed class MultiModGroup(Mod mod) : IModGroup { public GroupType Type => GroupType.Multi; + public Mod Mod { get; set; } = mod; public string Name { get; set; } = "Group"; public string Description { get; set; } = "A non-exclusive group of settings."; public ModPriority Priority { get; set; } @@ -26,27 +26,58 @@ public sealed class MultiModGroup : IModGroup .SelectWhere(o => (o.Mod.FileData.TryGetValue(gamePath, out var file) || o.Mod.FileSwapData.TryGetValue(gamePath, out file), file)) .FirstOrDefault(); - public SubMod this[Index idx] - => PrioritizedOptions[idx].Mod; + public int AddOption(Mod mod, string name, string description = "") + { + var groupIdx = mod.Groups.IndexOf(this); + if (groupIdx < 0) + return -1; + + var subMod = new SubMod(mod, this) + { + Name = name, + Description = description, + }; + PrioritizedOptions.Add((subMod, ModPriority.Default)); + return PrioritizedOptions.Count - 1; + } + + public bool ChangeOptionDescription(int optionIndex, string newDescription) + { + if (optionIndex < 0 || optionIndex >= PrioritizedOptions.Count) + return false; + + var option = PrioritizedOptions[optionIndex].Mod; + if (option.Description == newDescription) + return false; + + option.Description = newDescription; + return true; + } + + public bool ChangeOptionName(int optionIndex, string newName) + { + if (optionIndex < 0 || optionIndex >= PrioritizedOptions.Count) + return false; + + var option = PrioritizedOptions[optionIndex].Mod; + if (option.Name == newName) + return false; + + option.Name = newName; + return true; + } + + public IReadOnlyList Options + => PrioritizedOptions.Select(p => p.Mod).ToArray(); public bool IsOption - => Count > 0; - - [JsonIgnore] - public int Count - => PrioritizedOptions.Count; + => PrioritizedOptions.Count > 0; public readonly List<(SubMod Mod, ModPriority Priority)> PrioritizedOptions = []; - public IEnumerator GetEnumerator() - => PrioritizedOptions.Select(o => o.Mod).GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - public static MultiModGroup? Load(Mod mod, JObject json, int groupIdx) { - var ret = new MultiModGroup() + var ret = new MultiModGroup(mod) { Name = json[nameof(Name)]?.ToObject() ?? string.Empty, Description = json[nameof(Description)]?.ToObject() ?? string.Empty, @@ -68,8 +99,7 @@ public sealed class MultiModGroup : IModGroup break; } - var subMod = new SubMod(mod); - subMod.SetPosition(groupIdx, ret.PrioritizedOptions.Count); + var subMod = new SubMod(mod, ret); subMod.Load(mod.ModPath, child, out var priority); ret.PrioritizedOptions.Add((subMod, priority)); } @@ -85,12 +115,12 @@ public sealed class MultiModGroup : IModGroup { case GroupType.Multi: return this; case GroupType.Single: - var multi = new SingleModGroup() + var multi = new SingleModGroup(Mod) { Name = Name, Description = Description, Priority = Priority, - DefaultSettings = DefaultSettings.TurnMulti(Count), + DefaultSettings = DefaultSettings.TurnMulti(PrioritizedOptions.Count), }; multi.OptionData.AddRange(PrioritizedOptions.Select(p => p.Mod)); return multi; @@ -104,16 +134,9 @@ public sealed class MultiModGroup : IModGroup return false; DefaultSettings = DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo); - UpdatePositions(Math.Min(optionIdxFrom, optionIdxTo)); return true; } - public void UpdatePositions(int from = 0) - { - foreach (var ((o, _), i) in PrioritizedOptions.WithIndex().Skip(from)) - o.SetPosition(o.GroupIdx, i); - } - public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) { foreach (var (option, index) in PrioritizedOptions.WithIndex().OrderByDescending(o => o.Value.Priority)) @@ -124,5 +147,12 @@ public sealed class MultiModGroup : IModGroup } public Setting FixSetting(Setting setting) - => new(setting.Value & ((1ul << Count) - 1)); + => new(setting.Value & ((1ul << PrioritizedOptions.Count) - 1)); + + /// Create a group without a mod only for saving it in the creator. + internal static MultiModGroup CreateForSaving(string name) + => new(null!) + { + Name = name, + }; } diff --git a/Penumbra/Mods/Subclasses/SingleModGroup.cs b/Penumbra/Mods/Subclasses/SingleModGroup.cs index b854d2b1..994a1f96 100644 --- a/Penumbra/Mods/Subclasses/SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/SingleModGroup.cs @@ -1,4 +1,3 @@ -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Filesystem; @@ -9,11 +8,12 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; /// Groups that allow only one of their available options to be selected. -public sealed class SingleModGroup : IModGroup +public sealed class SingleModGroup(Mod mod) : IModGroup { public GroupType Type => GroupType.Single; + public Mod Mod { get; set; } = mod; public string Name { get; set; } = "Option"; public string Description { get; set; } = "A mutually exclusive group of settings."; public ModPriority Priority { get; set; } @@ -26,26 +26,53 @@ public sealed class SingleModGroup : IModGroup .SelectWhere(m => (m.FileData.TryGetValue(gamePath, out var file) || m.FileSwapData.TryGetValue(gamePath, out file), file)) .FirstOrDefault(); - public SubMod this[Index idx] - => OptionData[idx]; + public int AddOption(Mod mod, string name, string description = "") + { + var subMod = new SubMod(mod, this) + { + Name = name, + Description = description, + }; + OptionData.Add(subMod); + return OptionData.Count - 1; + } + + public bool ChangeOptionDescription(int optionIndex, string newDescription) + { + if (optionIndex < 0 || optionIndex >= OptionData.Count) + return false; + + var option = OptionData[optionIndex]; + if (option.Description == newDescription) + return false; + + option.Description = newDescription; + return true; + } + + public bool ChangeOptionName(int optionIndex, string newName) + { + if (optionIndex < 0 || optionIndex >= OptionData.Count) + return false; + + var option = OptionData[optionIndex]; + if (option.Name == newName) + return false; + + option.Name = newName; + return true; + } + + public IReadOnlyList Options + => OptionData; public bool IsOption - => Count > 1; - - [JsonIgnore] - public int Count - => OptionData.Count; - - public IEnumerator GetEnumerator() - => OptionData.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); + => OptionData.Count > 1; public static SingleModGroup? Load(Mod mod, JObject json, int groupIdx) { var options = json["Options"]; - var ret = new SingleModGroup + var ret = new SingleModGroup(mod) { Name = json[nameof(Name)]?.ToObject() ?? string.Empty, Description = json[nameof(Description)]?.ToObject() ?? string.Empty, @@ -58,8 +85,7 @@ public sealed class SingleModGroup : IModGroup if (options != null) foreach (var child in options.Children()) { - var subMod = new SubMod(mod); - subMod.SetPosition(groupIdx, ret.OptionData.Count); + var subMod = new SubMod(mod, ret); subMod.Load(mod.ModPath, child, out _); ret.OptionData.Add(subMod); } @@ -74,7 +100,7 @@ public sealed class SingleModGroup : IModGroup { case GroupType.Single: return this; case GroupType.Multi: - var multi = new MultiModGroup() + var multi = new MultiModGroup(Mod) { Name = Name, Description = Description, @@ -108,19 +134,19 @@ public sealed class SingleModGroup : IModGroup DefaultSettings = Setting.Single(currentIndex + 1); } - UpdatePositions(Math.Min(optionIdxFrom, optionIdxTo)); return true; } - public void UpdatePositions(int from = 0) - { - foreach (var (o, i) in OptionData.WithIndex().Skip(from)) - o.SetPosition(o.GroupIdx, i); - } - public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) - => this[setting.AsIndex].AddData(redirections, manipulations); + => OptionData[setting.AsIndex].AddData(redirections, manipulations); public Setting FixSetting(Setting setting) - => Count == 0 ? Setting.Zero : new Setting(Math.Min(setting.Value, (ulong)(Count - 1))); + => OptionData.Count == 0 ? Setting.Zero : new Setting(Math.Min(setting.Value, (ulong)(OptionData.Count - 1))); + + /// Create a group without a mod only for saving it in the creator. + internal static SingleModGroup CreateForSaving(string name) + => new(null!) + { + Name = name, + }; } diff --git a/Penumbra/Mods/Subclasses/SubMod.cs b/Penumbra/Mods/Subclasses/SubMod.cs index 386910e5..bc93fcc4 100644 --- a/Penumbra/Mods/Subclasses/SubMod.cs +++ b/Penumbra/Mods/Subclasses/SubMod.cs @@ -1,11 +1,62 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using OtterGui; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; +public class SingleSubMod(Mod mod, SingleModGroup group) : IModOption, IModDataContainer +{ + internal readonly Mod Mod = mod; + internal readonly SingleModGroup Group = group; + + public string Name { get; set; } = "Option"; + + public string FullName + => $"{Group.Name}: {Name}"; + + public string Description { get; set; } = string.Empty; + + public Dictionary Files { get; set; } = []; + public Dictionary FileSwaps { get; set; } = []; + public HashSet Manipulations { get; set; } = []; +} + +public class MultiSubMod(Mod mod, MultiModGroup group) : IModOption, IModDataContainer +{ + internal readonly Mod Mod = mod; + internal readonly MultiModGroup Group = group; + + public string Name { get; set; } = "Option"; + + public string FullName + => $"{Group.Name}: {Name}"; + + public string Description { get; set; } = string.Empty; + public ModPriority Priority { get; set; } = ModPriority.Default; + + public Dictionary Files { get; set; } = []; + public Dictionary FileSwaps { get; set; } = []; + public HashSet Manipulations { get; set; } = []; +} + +public class DefaultSubMod(IMod mod) : IModDataContainer +{ + public string FullName + => "Default Option"; + + public string Description + => string.Empty; + + internal readonly IMod Mod = mod; + + public Dictionary Files { get; set; } = []; + public Dictionary FileSwaps { get; set; } = []; + public HashSet Manipulations { get; set; } = []; +} + /// /// A sub mod is a collection of /// - file replacements @@ -16,21 +67,51 @@ namespace Penumbra.Mods.Subclasses; /// Nothing is checked for existence or validity when loading. /// Objects are also not checked for uniqueness, the first appearance of a game path or meta path decides. /// -public sealed class SubMod +public sealed class SubMod(IMod mod, IModGroup group) : IModOption { public string Name { get; set; } = "Default"; public string FullName - => GroupIdx < 0 ? "Default Option" : $"{ParentMod.Groups[GroupIdx].Name}: {Name}"; + => Group == null ? "Default Option" : $"{Group.Name}: {Name}"; public string Description { get; set; } = string.Empty; - internal IMod ParentMod { get; private init; } - internal int GroupIdx { get; private set; } - internal int OptionIdx { get; private set; } + internal readonly IMod Mod = mod; + internal readonly IModGroup? Group = group; + internal (int GroupIdx, int OptionIdx) GetIndices() + { + if (IsDefault) + return (-1, 0); + + var groupIdx = Mod.Groups.IndexOf(Group); + if (groupIdx < 0) + throw new Exception($"Group {Group.Name} from SubMod {Name} is not contained in Mod {Mod.Name}."); + + return (groupIdx, GetOptionIndex()); + } + + private int GetOptionIndex() + { + var optionIndex = Group switch + { + null => 0, + SingleModGroup single => single.OptionData.IndexOf(this), + MultiModGroup multi => multi.PrioritizedOptions.IndexOf(p => p.Mod == this), + _ => throw new Exception($"Group {Group.Name} from SubMod {Name} has unknown type {typeof(Group)}"), + }; + if (optionIndex < 0) + throw new Exception($"Group {Group!.Name} from SubMod {Name} does not contain this SubMod."); + + return optionIndex; + } + + public static SubMod CreateDefault(IMod mod) + => new(mod, null!); + + [MemberNotNullWhen(false, nameof(Group))] public bool IsDefault - => GroupIdx < 0; + => Group == null; public void AddData(Dictionary redirections, HashSet manipulations) { @@ -46,9 +127,6 @@ public sealed class SubMod public Dictionary FileSwapData = []; public HashSet ManipulationData = []; - public SubMod(IMod parentMod) - => ParentMod = parentMod; - public IReadOnlyDictionary Files => FileData; @@ -58,12 +136,6 @@ public sealed class SubMod public IReadOnlySet Manipulations => ManipulationData; - public void SetPosition(int groupIdx, int optionIdx) - { - GroupIdx = groupIdx; - OptionIdx = optionIdx; - } - public void Load(DirectoryInfo basePath, JToken json, out ModPriority priority) { FileData.Clear(); @@ -116,6 +188,14 @@ public sealed class SubMod } } + /// Create a sub mod without a mod or group only for saving it in the creator. + internal static SubMod CreateForSaving(string name) + => new(null!, null!) + { + Name = name, + }; + + public static void WriteSubMod(JsonWriter j, JsonSerializer serializer, SubMod mod, DirectoryInfo basePath, ModPriority? priority) { j.WriteStartObject(); diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index 41c1211f..a599b3bb 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -49,7 +49,7 @@ public class TemporaryMod : IMod => [Default]; public TemporaryMod() - => Default = new SubMod(this); + => Default = SubMod.CreateDefault(this); public void SetFile(Utf8GamePath gamePath, FullPath fullPath) => Default.FileData[gamePath] = fullPath; diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 7b5ce2dc..5125a5b2 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -253,7 +253,7 @@ public class ItemSwapTab : IDisposable, ITab _subModValid = _mod != null && _newGroupName.Length > 0 && _newOptionName.Length > 0 - && (_selectedGroup?.All(o => o.Name != _newOptionName) ?? true); + && (_selectedGroup?.Options.All(o => o.Name != _newOptionName) ?? true); } private void CreateMod() @@ -275,7 +275,7 @@ public class ItemSwapTab : IDisposable, ITab var groupCreated = false; var dirCreated = false; - var optionCreated = false; + var optionCreated = -1; DirectoryInfo? optionFolderName = null; try { @@ -294,14 +294,17 @@ public class ItemSwapTab : IDisposable, ITab groupCreated = true; } - _modManager.OptionEditor.AddOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _newOptionName); - optionCreated = true; + var optionIdx = _modManager.OptionEditor.AddOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _newOptionName); + if (optionIdx < 0) + throw new Exception($"Failure creating mod option."); + + optionCreated = optionIdx; optionFolderName = Directory.CreateDirectory(optionFolderName.FullName); dirCreated = true; if (!_swapData.WriteMod(_modManager, _mod, _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps, optionFolderName, - _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1)) + _mod.Groups.IndexOf(_selectedGroup), optionIdx)) throw new Exception("Failure writing files for mod swap."); } } @@ -310,8 +313,8 @@ public class ItemSwapTab : IDisposable, ITab Penumbra.Messager.NotificationMessage(e, "Could not create new Swap Option.", NotificationType.Error, false); try { - if (optionCreated && _selectedGroup != null) - _modManager.OptionEditor.DeleteOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1); + if (optionCreated >= 0 && _selectedGroup != null) + _modManager.OptionEditor.DeleteOption(_mod, _mod.Groups.IndexOf(_selectedGroup), optionCreated); if (groupCreated) { diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index a70da628..3f5f6c37 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -78,7 +78,10 @@ public partial class ModEditWindow : Window, IDisposable } public void ChangeOption(SubMod? subMod) - => _editor.LoadOption(subMod?.GroupIdx ?? -1, subMod?.OptionIdx ?? 0); + { + var (groupIdx, optionIdx) = subMod?.GetIndices() ?? (-1, 0); + _editor.LoadOption(groupIdx, optionIdx); + } public void UpdateModels() { @@ -428,7 +431,8 @@ public partial class ModEditWindow : Window, IDisposable using var id = ImRaii.PushId(idx); if (ImGui.Selectable(option.FullName, option == _editor.Option)) { - _editor.LoadOption(option.GroupIdx, option.OptionIdx); + var (groupIdx, optionIdx) = option.GetIndices(); + _editor.LoadOption(groupIdx, optionIdx); ret = true; } } @@ -565,7 +569,7 @@ public partial class ModEditWindow : Window, IDisposable } if (Mod != null) - foreach (var option in Mod.Groups.SelectMany(g => g).Append(Mod.Default)) + foreach (var option in Mod.AllSubMods) { foreach (var path in option.Files.Keys) { diff --git a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs index 1df814da..c34c7ef0 100644 --- a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs @@ -71,7 +71,7 @@ public class ModMergeTab(ModMerger modMerger) color = color == Colors.DiscordColor ? Colors.DiscordColor - : group == null || group.Any(o => o.Name == modMerger.OptionName) + : group == null || group.Options.Any(o => o.Name == modMerger.OptionName) ? Colors.PressEnterWarningBg : Colors.DiscordColor; c.Push(ImGuiCol.Border, color); @@ -184,18 +184,26 @@ public class ModMergeTab(ModMerger modMerger) else { ImGuiUtil.DrawTableColumn(option.Name); - var group = option.ParentMod.Groups[option.GroupIdx]; + var group = option.Group; + var optionEnumerator = group switch + { + SingleModGroup single => single.OptionData, + MultiModGroup multi => multi.PrioritizedOptions.Select(o => o.Mod), + _ => [], + }; ImGui.TableNextColumn(); ImGui.Selectable(group.Name, false); if (ImGui.BeginPopupContextItem("##groupContext")) { if (ImGui.MenuItem("Select All")) - foreach (var opt in group) - Handle((SubMod)opt, true); + // ReSharper disable once PossibleMultipleEnumeration + foreach (var opt in optionEnumerator) + Handle(opt, true); if (ImGui.MenuItem("Unselect All")) - foreach (var opt in group) - Handle((SubMod)opt, false); + // ReSharper disable once PossibleMultipleEnumeration + foreach (var opt in optionEnumerator) + Handle(opt, false); ImGui.EndPopup(); } } diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index b002dedd..0dc694d8 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -324,7 +324,7 @@ public class ModPanelEditTab( ? mod.Description : optionIdx < 0 ? mod.Groups[groupIdx].Description - : mod.Groups[groupIdx][optionIdx].Description; + : mod.Groups[groupIdx].Options[optionIdx].Description; _oldDescription = _newDescription; _mod = mod; @@ -479,17 +479,24 @@ public class ModPanelEditTab( ImGui.TableSetupColumn("delete", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); ImGui.TableSetupColumn("priority", ImGuiTableColumnFlags.WidthFixed, 50 * UiHelpers.Scale); - var group = panel._mod.Groups[groupIdx]; - for (var optionIdx = 0; optionIdx < group.Count; ++optionIdx) - EditOption(panel, group, groupIdx, optionIdx); - + switch (panel._mod.Groups[groupIdx]) + { + case SingleModGroup single: + for (var optionIdx = 0; optionIdx < single.OptionData.Count; ++optionIdx) + EditOption(panel, single, groupIdx, optionIdx); + break; + case MultiModGroup multi: + for (var optionIdx = 0; optionIdx < multi.PrioritizedOptions.Count; ++optionIdx) + EditOption(panel, multi, groupIdx, optionIdx); + break; + } DrawNewOption(panel, groupIdx, UiHelpers.IconButtonSize); } /// Draw a line for a single option. private static void EditOption(ModPanelEditTab panel, IModGroup group, int groupIdx, int optionIdx) { - var option = group[optionIdx]; + var option = group.Options[optionIdx]; using var id = ImRaii.PushId(optionIdx); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); @@ -547,10 +554,16 @@ public class ModPanelEditTab( { var mod = panel._mod; var group = mod.Groups[groupIdx]; + var count = group switch + { + SingleModGroup single => single.OptionData.Count, + MultiModGroup multi => multi.PrioritizedOptions.Count, + _ => throw new Exception($"Dragging options to an option group of type {group.GetType()} is not supported."), + }; ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); - ImGui.Selectable($"Option #{group.Count + 1}"); - Target(panel, group, groupIdx, group.Count); + ImGui.Selectable($"Option #{count + 1}"); + Target(panel, group, groupIdx, count); ImGui.TableNextColumn(); ImGui.TableNextColumn(); ImGui.SetNextItemWidth(-1); @@ -562,7 +575,7 @@ public class ModPanelEditTab( } ImGui.TableNextColumn(); - var canAddGroup = mod.Groups[groupIdx].Type != GroupType.Multi || mod.Groups[groupIdx].Count < IModGroup.MaxMultiOptions; + var canAddGroup = mod.Groups[groupIdx].Type != GroupType.Multi || count < IModGroup.MaxMultiOptions; var validName = _newOptionName.Length > 0 && _newOptionNameIdx == groupIdx; var tt = canAddGroup ? validName ? "Add a new option to this group." : "Please enter a name for the new option." @@ -588,7 +601,7 @@ public class ModPanelEditTab( _dragDropOptionIdx = optionIdx; } - ImGui.TextUnformatted($"Dragging option {group[optionIdx].Name} from group {group.Name}..."); + ImGui.TextUnformatted($"Dragging option {group.Options[optionIdx].Name} from group {group.Name}..."); } private static void Target(ModPanelEditTab panel, IModGroup group, int groupIdx, int optionIdx) @@ -611,12 +624,17 @@ public class ModPanelEditTab( var sourceGroupIdx = _dragDropGroupIdx; var sourceOption = _dragDropOptionIdx; var sourceGroup = panel._mod.Groups[sourceGroupIdx]; - var currentCount = group.Count; - var option = sourceGroup[sourceOption]; - var priority = sourceGroup switch + var currentCount = group switch { - MultiModGroup multi => multi.PrioritizedOptions[_dragDropOptionIdx].Priority, - _ => ModPriority.Default, + SingleModGroup single => single.OptionData.Count, + MultiModGroup multi => multi.PrioritizedOptions.Count, + _ => throw new Exception($"Dragging options to an option group of type {group.GetType()} is not supported."), + }; + var (option, priority) = sourceGroup switch + { + SingleModGroup single => (single.OptionData[_dragDropOptionIdx], ModPriority.Default), + MultiModGroup multi => multi.PrioritizedOptions[_dragDropOptionIdx], + _ => throw new Exception($"Dragging options from an option group of type {sourceGroup.GetType()} is not supported."), }; panel._delayedActions.Enqueue(() => { @@ -651,7 +669,7 @@ public class ModPanelEditTab( if (ImGui.Selectable(GroupTypeName(GroupType.Single), group.Type == GroupType.Single)) _modManager.OptionEditor.ChangeModGroupType(_mod, groupIdx, GroupType.Single); - var canSwitchToMulti = group.Count <= IModGroup.MaxMultiOptions; + var canSwitchToMulti = group.Options.Count <= IModGroup.MaxMultiOptions; using var style = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, !canSwitchToMulti); if (ImGui.Selectable(GroupTypeName(GroupType.Multi), group.Type == GroupType.Multi) && canSwitchToMulti) _modManager.OptionEditor.ChangeModGroupType(_mod, groupIdx, GroupType.Multi); diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index 1107aa20..cb76088c 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -75,7 +75,7 @@ public class ModPanelSettingsTab : ITab { var useDummy = true; foreach (var (group, idx) in _selector.Selected!.Groups.WithIndex() - .Where(g => g.Value.Type == GroupType.Single && g.Value.Count > _config.SingleGroupRadioMax)) + .Where(g => g.Value.Type == GroupType.Single && g.Value.Options.Count > _config.SingleGroupRadioMax)) { ImGuiUtil.Dummy(UiHelpers.DefaultSpace, useDummy); useDummy = false; @@ -92,7 +92,7 @@ public class ModPanelSettingsTab : ITab case GroupType.Multi: DrawMultiGroup(group, idx); break; - case GroupType.Single when group.Count <= _config.SingleGroupRadioMax: + case GroupType.Single when group.Options.Count <= _config.SingleGroupRadioMax: DrawSingleGroupRadio(group, idx); break; } @@ -181,13 +181,14 @@ public class ModPanelSettingsTab : ITab using var id = ImRaii.PushId(groupIdx); var selectedOption = _empty ? group.DefaultSettings.AsIndex : _settings.Settings[groupIdx].AsIndex; ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X * 3 / 4); - using (var combo = ImRaii.Combo(string.Empty, group[selectedOption].Name)) + var options = group.Options; + using (var combo = ImRaii.Combo(string.Empty, options[selectedOption].Name)) { if (combo) - for (var idx2 = 0; idx2 < group.Count; ++idx2) + for (var idx2 = 0; idx2 < options.Count; ++idx2) { id.Push(idx2); - var option = group[idx2]; + var option = options[idx2]; if (ImGui.Selectable(option.Name, idx2 == selectedOption)) _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, Setting.Single(idx2)); @@ -213,18 +214,18 @@ public class ModPanelSettingsTab : ITab using var id = ImRaii.PushId(groupIdx); var selectedOption = _empty ? group.DefaultSettings.AsIndex : _settings.Settings[groupIdx].AsIndex; var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); - - DrawCollapseHandling(group, minWidth, DrawOptions); + var options = group.Options; + DrawCollapseHandling(options, minWidth, DrawOptions); Widget.EndFramedGroup(); return; void DrawOptions() { - for (var idx = 0; idx < group.Count; ++idx) + for (var idx = 0; idx < group.Options.Count; ++idx) { using var i = ImRaii.PushId(idx); - var option = group[idx]; + var option = options[idx]; if (ImGui.RadioButton(option.Name, selectedOption == idx)) _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, Setting.Single(idx)); @@ -239,9 +240,9 @@ public class ModPanelSettingsTab : ITab } - private void DrawCollapseHandling(IModGroup group, float minWidth, Action draw) + private void DrawCollapseHandling(IReadOnlyList options, float minWidth, Action draw) { - if (group.Count <= _config.OptionGroupCollapsibleMin) + if (options.Count <= _config.OptionGroupCollapsibleMin) { draw(); } @@ -249,8 +250,8 @@ public class ModPanelSettingsTab : ITab { var collapseId = ImGui.GetID("Collapse"); var shown = ImGui.GetStateStorage().GetBool(collapseId, true); - var buttonTextShow = $"Show {group.Count} Options"; - var buttonTextHide = $"Hide {group.Count} Options"; + var buttonTextShow = $"Show {options.Count} Options"; + var buttonTextHide = $"Hide {options.Count} Options"; var buttonWidth = Math.Max(ImGui.CalcTextSize(buttonTextShow).X, ImGui.CalcTextSize(buttonTextHide).X) + 2 * ImGui.GetStyle().FramePadding.X; minWidth = Math.Max(buttonWidth, minWidth); @@ -274,7 +275,7 @@ public class ModPanelSettingsTab : ITab } else { - var optionWidth = group.Max(o => ImGui.CalcTextSize(o.Name).X) + var optionWidth = options.Max(o => ImGui.CalcTextSize(o.Name).X) + ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetFrameHeight() + ImGui.GetStyle().FramePadding.X; @@ -294,8 +295,8 @@ public class ModPanelSettingsTab : ITab using var id = ImRaii.PushId(groupIdx); var flags = _empty ? group.DefaultSettings : _settings.Settings[groupIdx]; var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); - - DrawCollapseHandling(group, minWidth, DrawOptions); + var options = group.Options; + DrawCollapseHandling(options, minWidth, DrawOptions); Widget.EndFramedGroup(); var label = $"##multi{groupIdx}"; @@ -307,10 +308,10 @@ public class ModPanelSettingsTab : ITab void DrawOptions() { - for (var idx = 0; idx < group.Count; ++idx) + for (var idx = 0; idx < options.Count; ++idx) { using var i = ImRaii.PushId(idx); - var option = group[idx]; + var option = options[idx]; var setting = flags.HasFlag(idx); if (ImGui.Checkbox(option.Name, ref setting)) @@ -339,7 +340,7 @@ public class ModPanelSettingsTab : ITab ImGui.Separator(); if (ImGui.Selectable("Enable All")) _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, - Setting.AllBits(group.Count)); + Setting.AllBits(group.Options.Count)); if (ImGui.Selectable("Disable All")) _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, Setting.Zero);