From 6b1743b776da300fbe1b80a16e662b66ea519a42 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 24 Apr 2024 23:04:04 +0200 Subject: [PATCH] This sucks so hard... --- Penumbra/Api/Api/ModSettingsApi.cs | 2 +- Penumbra/Import/TexToolsImporter.ModPack.cs | 6 +- Penumbra/Meta/MetaFileManager.cs | 14 +- Penumbra/Mods/Editor/DuplicateManager.cs | 6 +- Penumbra/Mods/Editor/FileRegistry.cs | 12 +- Penumbra/Mods/Editor/ModEditor.cs | 63 +-- Penumbra/Mods/Editor/ModFileCollection.cs | 16 +- Penumbra/Mods/Editor/ModFileEditor.cs | 22 +- Penumbra/Mods/Editor/ModMerger.cs | 79 ++- Penumbra/Mods/Editor/ModMetaEditor.cs | 4 +- Penumbra/Mods/Editor/ModNormalizer.cs | 38 +- Penumbra/Mods/Editor/ModSwapEditor.cs | 2 +- Penumbra/Mods/Manager/ModCacheManager.cs | 29 +- Penumbra/Mods/Manager/ModMigration.cs | 39 +- Penumbra/Mods/Manager/ModOptionEditor.cs | 144 +++--- Penumbra/Mods/Mod.cs | 19 +- Penumbra/Mods/ModCreator.cs | 41 +- Penumbra/Mods/Subclasses/IModDataContainer.cs | 48 ++ Penumbra/Mods/Subclasses/IModGroup.cs | 126 +++-- Penumbra/Mods/Subclasses/IModOption.cs | 2 + Penumbra/Mods/Subclasses/MultiModGroup.cs | 122 ++--- Penumbra/Mods/Subclasses/SingleModGroup.cs | 75 +-- Penumbra/Mods/Subclasses/SubMod.cs | 477 +++++++++++------- Penumbra/Mods/TemporaryMod.cs | 35 +- Penumbra/UI/AdvancedWindow/FileEditor.cs | 2 +- .../UI/AdvancedWindow/ModEditWindow.Files.cs | 15 +- .../UI/AdvancedWindow/ModEditWindow.Meta.cs | 2 +- .../ModEditWindow.Models.MdlTab.cs | 4 +- .../ModEditWindow.QuickImport.cs | 12 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 28 +- Penumbra/UI/AdvancedWindow/ModMergeTab.cs | 33 +- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 28 +- Penumbra/UI/ModsTab/ModPanelTabBar.cs | 2 +- 33 files changed, 852 insertions(+), 695 deletions(-) diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs index 5ed26ce5..e145e027 100644 --- a/Penumbra/Api/Api/ModSettingsApi.cs +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -201,7 +201,7 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable { foreach (var name in optionNames) { - var optionIdx = multi.PrioritizedOptions.IndexOf(o => o.Mod.Name == name); + var optionIdx = multi.OptionData.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 7d9388a9..b9cdda71 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -152,7 +152,7 @@ public partial class TexToolsImporter } // Iterate through all pages - var options = new List(); + var options = new List(); var groupPriority = ModPriority.Default; var groupNames = new HashSet(); foreach (var page in modList.ModPackPages) @@ -183,7 +183,7 @@ public partial class TexToolsImporter var optionFolder = ModCreator.NewSubFolderName(groupFolder, option.Name, _config.ReplaceNonAsciiOnImport) ?? new DirectoryInfo(Path.Combine(groupFolder.FullName, $"Option {i + optionIdx + 1}")); ExtractSimpleModList(optionFolder, option.ModsJsons); - options.Add(_modManager.Creator.CreateSubMod(_currentModDirectory, optionFolder, option)); + options.Add(_modManager.Creator.CreateSubMod(_currentModDirectory, optionFolder, option, new ModPriority(i))); if (option.IsChecked) defaultSettings = group.SelectionType == GroupType.Multi ? defaultSettings!.Value | Setting.Multi(i) @@ -203,7 +203,7 @@ public partial class TexToolsImporter { var option = group.OptionList[idx]; _currentOptionName = option.Name; - options.Insert(idx, SubMod.CreateForSaving(option.Name)); + options.Insert(idx, MultiSubMod.CreateForSaving(option.Name, option.Description, ModPriority.Default)); if (option.IsChecked) defaultSettings = Setting.Single(idx); } diff --git a/Penumbra/Meta/MetaFileManager.cs b/Penumbra/Meta/MetaFileManager.cs index b1823bd7..0e2e638b 100644 --- a/Penumbra/Meta/MetaFileManager.cs +++ b/Penumbra/Meta/MetaFileManager.cs @@ -50,17 +50,15 @@ public unsafe class MetaFileManager TexToolsMeta.WriteTexToolsMeta(this, mod.Default.Manipulations, mod.ModPath); foreach (var group in mod.Groups) { + if (group is not ITexToolsGroup texToolsGroup) + continue; + var dir = ModCreator.NewOptionDirectory(mod.ModPath, group.Name, Config.ReplaceNonAsciiOnImport); if (!dir.Exists) dir.Create(); - var optionEnumerator = group switch - { - SingleModGroup single => single.OptionData, - MultiModGroup multi => multi.PrioritizedOptions.Select(o => o.Mod), - _ => [], - }; - foreach (var option in optionEnumerator) + + foreach (var option in texToolsGroup.OptionData) { var optionDir = ModCreator.NewOptionDirectory(dir, option.Name, Config.ReplaceNonAsciiOnImport); if (!optionDir.Exists) @@ -99,7 +97,7 @@ public unsafe class MetaFileManager return; ResidentResources.Reload(); - if (collection?._cache == null) + if (collection._cache == null) CharacterUtility.ResetAll(); else collection._cache.Meta.SetFiles(); diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index 938199aa..92ec58b9 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -29,7 +29,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co Worker = Task.Run(() => CheckDuplicates(filesTmp, _cancellationTokenSource.Token), _cancellationTokenSource.Token); } - public void DeleteDuplicates(ModFileCollection files, Mod mod, SubMod option, bool useModManager) + public void DeleteDuplicates(ModFileCollection files, Mod mod, IModDataContainer option, bool useModManager) { if (!Worker.IsCompleted || _duplicates.Count == 0) return; @@ -72,7 +72,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co return; - void HandleSubMod(SubMod subMod, int groupIdx, int optionIdx) + void HandleSubMod(IModDataContainer subMod, int groupIdx, int optionIdx) { var changes = false; var dict = subMod.Files.ToDictionary(kvp => kvp.Key, @@ -86,7 +86,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co } else { - subMod.FileData = dict; + subMod.Files = dict; saveService.ImmediateSaveSync(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); } } diff --git a/Penumbra/Mods/Editor/FileRegistry.cs b/Penumbra/Mods/Editor/FileRegistry.cs index 427c58ca..44d349ce 100644 --- a/Penumbra/Mods/Editor/FileRegistry.cs +++ b/Penumbra/Mods/Editor/FileRegistry.cs @@ -5,12 +5,12 @@ namespace Penumbra.Mods.Editor; public class FileRegistry : IEquatable { - public readonly List<(SubMod, Utf8GamePath)> SubModUsage = []; - public FullPath File { get; private init; } - public Utf8RelPath RelPath { get; private init; } - public long FileSize { get; private init; } - public int CurrentUsage; - public bool IsOnPlayer; + public readonly List<(IModDataContainer, Utf8GamePath)> SubModUsage = []; + public FullPath File { get; private init; } + public Utf8RelPath RelPath { get; private init; } + public long FileSize { get; private init; } + public int CurrentUsage; + public bool IsOnPlayer; public static bool FromFile(DirectoryInfo modPath, FileInfo file, [NotNullWhen(true)] out FileRegistry? registry) { diff --git a/Penumbra/Mods/Editor/ModEditor.cs b/Penumbra/Mods/Editor/ModEditor.cs index 0a96e0fd..1118f890 100644 --- a/Penumbra/Mods/Editor/ModEditor.cs +++ b/Penumbra/Mods/Editor/ModEditor.cs @@ -1,4 +1,3 @@ -using System; using OtterGui; using OtterGui.Compression; using Penumbra.Mods.Subclasses; @@ -25,20 +24,20 @@ public class ModEditor( public readonly MdlMaterialEditor MdlMaterialEditor = mdlMaterialEditor; public readonly FileCompactor Compactor = compactor; - public Mod? Mod { get; private set; } - public int GroupIdx { get; private set; } - public int OptionIdx { get; private set; } + public Mod? Mod { get; private set; } + public int GroupIdx { get; private set; } + public int DataIdx { get; private set; } - public IModGroup? Group { get; private set; } - public SubMod? Option { get; private set; } + public IModGroup? Group { get; private set; } + public IModDataContainer? Option { get; private set; } public void LoadMod(Mod mod) => LoadMod(mod, -1, 0); - public void LoadMod(Mod mod, int groupIdx, int optionIdx) + public void LoadMod(Mod mod, int groupIdx, int dataIdx) { Mod = mod; - LoadOption(groupIdx, optionIdx, true); + LoadOption(groupIdx, dataIdx, true); Files.UpdateAll(mod, Option!); SwapEditor.Revert(Option!); MetaEditor.Load(Mod!, Option!); @@ -46,9 +45,9 @@ public class ModEditor( MdlMaterialEditor.ScanModels(Mod!); } - public void LoadOption(int groupIdx, int optionIdx) + public void LoadOption(int groupIdx, int dataIdx) { - LoadOption(groupIdx, optionIdx, true); + LoadOption(groupIdx, dataIdx, true); SwapEditor.Revert(Option!); Files.UpdatePaths(Mod!, Option!); MetaEditor.Load(Mod!, Option!); @@ -57,44 +56,38 @@ public class ModEditor( } /// Load the correct option by indices for the currently loaded mod if possible, unload if not. - private void LoadOption(int groupIdx, int optionIdx, bool message) + private void LoadOption(int groupIdx, int dataIdx, bool message) { if (Mod != null && Mod.Groups.Count > groupIdx) { - if (groupIdx == -1 && optionIdx == 0) + if (groupIdx == -1 && dataIdx == 0) { - Group = null; - Option = Mod.Default; - GroupIdx = groupIdx; - OptionIdx = optionIdx; + Group = null; + Option = Mod.Default; + GroupIdx = groupIdx; + DataIdx = dataIdx; return; } if (groupIdx >= 0) { Group = Mod.Groups[groupIdx]; - switch(Group) + if (dataIdx >= 0 && dataIdx < Group.DataContainers.Count) { - 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; + Option = Group.DataContainers[dataIdx]; + GroupIdx = groupIdx; + DataIdx = dataIdx; + return; } } } - Group = null; - Option = Mod?.Default; - GroupIdx = -1; - OptionIdx = 0; + Group = null; + Option = Mod?.Default; + GroupIdx = -1; + DataIdx = 0; if (message) - Penumbra.Log.Error($"Loading invalid option {groupIdx} {optionIdx} for Mod {Mod?.Name ?? "Unknown"}."); + Penumbra.Log.Error($"Loading invalid option {groupIdx} {dataIdx} for Mod {Mod?.Name ?? "Unknown"}."); } public void Clear() @@ -111,7 +104,7 @@ public class ModEditor( => Clear(); /// Apply a option action to all available option in a mod, including the default option. - public static void ApplyToAllOptions(Mod mod, Action action) + public static void ApplyToAllOptions(Mod mod, Action action) { action(mod.Default, -1, 0); foreach (var (group, groupIdx) in mod.Groups.WithIndex()) @@ -123,8 +116,8 @@ public class ModEditor( 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); + for (var optionIdx = 0; optionIdx < multi.OptionData.Count; ++optionIdx) + action(multi.OptionData[optionIdx], groupIdx, optionIdx); break; } } diff --git a/Penumbra/Mods/Editor/ModFileCollection.cs b/Penumbra/Mods/Editor/ModFileCollection.cs index 9dd78217..ede35914 100644 --- a/Penumbra/Mods/Editor/ModFileCollection.cs +++ b/Penumbra/Mods/Editor/ModFileCollection.cs @@ -38,13 +38,13 @@ public class ModFileCollection : IDisposable public bool Ready { get; private set; } = true; - public void UpdateAll(Mod mod, SubMod option) + public void UpdateAll(Mod mod, IModDataContainer option) { UpdateFiles(mod, new CancellationToken()); UpdatePaths(mod, option, false, new CancellationToken()); } - public void UpdatePaths(Mod mod, SubMod option) + public void UpdatePaths(Mod mod, IModDataContainer option) => UpdatePaths(mod, option, true, new CancellationToken()); public void Clear() @@ -59,7 +59,7 @@ public class ModFileCollection : IDisposable public void ClearMissingFiles() => _missing.Clear(); - public void RemoveUsedPath(SubMod option, FileRegistry? file, Utf8GamePath gamePath) + public void RemoveUsedPath(IModDataContainer option, FileRegistry? file, Utf8GamePath gamePath) { _usedPaths.Remove(gamePath); if (file != null) @@ -69,10 +69,10 @@ public class ModFileCollection : IDisposable } } - public void RemoveUsedPath(SubMod option, FullPath file, Utf8GamePath gamePath) + public void RemoveUsedPath(IModDataContainer option, FullPath file, Utf8GamePath gamePath) => RemoveUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath); - public void AddUsedPath(SubMod option, FileRegistry? file, Utf8GamePath gamePath) + public void AddUsedPath(IModDataContainer option, FileRegistry? file, Utf8GamePath gamePath) { _usedPaths.Add(gamePath); if (file == null) @@ -82,7 +82,7 @@ public class ModFileCollection : IDisposable file.SubModUsage.Add((option, gamePath)); } - public void AddUsedPath(SubMod option, FullPath file, Utf8GamePath gamePath) + public void AddUsedPath(IModDataContainer option, FullPath file, Utf8GamePath gamePath) => AddUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath); public void ChangeUsedPath(FileRegistry file, int pathIdx, Utf8GamePath gamePath) @@ -154,14 +154,14 @@ public class ModFileCollection : IDisposable _usedPaths.Clear(); } - private void UpdatePaths(Mod mod, SubMod option, bool clearRegistries, CancellationToken tok) + private void UpdatePaths(Mod mod, IModDataContainer option, bool clearRegistries, CancellationToken tok) { tok.ThrowIfCancellationRequested(); ClearPaths(clearRegistries, tok); tok.ThrowIfCancellationRequested(); - foreach (var subMod in mod.AllSubMods) + foreach (var subMod in mod.AllDataContainers) { foreach (var (gamePath, file) in subMod.Files) { diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index 51615b05..11e35334 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -14,7 +14,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu Changes = false; } - public int Apply(Mod mod, SubMod option) + public int Apply(Mod mod, IModDataContainer option) { var dict = new Dictionary(); var num = 0; @@ -24,23 +24,23 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu num += dict.TryAdd(path.Item2, file.File) ? 0 : 1; } - var (groupIdx, optionIdx) = option.GetIndices(); - modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, dict); + var (groupIdx, dataIdx) = option.GetDataIndices(); + modManager.OptionEditor.OptionSetFiles(mod, groupIdx, dataIdx, dict); files.UpdatePaths(mod, option); Changes = false; return num; } - public void Revert(Mod mod, SubMod option) + public void Revert(Mod mod, IModDataContainer option) { files.UpdateAll(mod, option); Changes = false; } /// Remove all path redirections where the pointed-to file does not exist. - public void RemoveMissingPaths(Mod mod, SubMod option) + public void RemoveMissingPaths(Mod mod, IModDataContainer option) { - void HandleSubMod(SubMod subMod, int groupIdx, int optionIdx) + void HandleSubMod(IModDataContainer subMod, int groupIdx, int optionIdx) { var newDict = subMod.Files.Where(kvp => CheckAgainstMissing(mod, subMod, kvp.Value, kvp.Key, subMod == option)) .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); @@ -62,7 +62,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu /// If path is empty, it will be deleted instead. /// If pathIdx is equal to the total number of paths, path will be added, otherwise replaced. /// - public bool SetGamePath(SubMod option, int fileIdx, int pathIdx, Utf8GamePath path) + public bool SetGamePath(IModDataContainer option, int fileIdx, int pathIdx, Utf8GamePath path) { if (!CanAddGamePath(path) || fileIdx < 0 || fileIdx > files.Available.Count) return false; @@ -85,7 +85,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu /// Transform a set of files to the appropriate game paths with the given number of folders skipped, /// and add them to the given option. /// - public int AddPathsToSelected(SubMod option, IEnumerable files1, int skipFolders = 0) + public int AddPathsToSelected(IModDataContainer option, IEnumerable files1, int skipFolders = 0) { var failed = 0; foreach (var file in files1) @@ -112,7 +112,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu } /// Remove all paths in the current option from the given files. - public void RemovePathsFromSelected(SubMod option, IEnumerable files1) + public void RemovePathsFromSelected(IModDataContainer option, IEnumerable files1) { foreach (var file in files1) { @@ -130,7 +130,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu } /// Delete all given files from your filesystem - public void DeleteFiles(Mod mod, SubMod option, IEnumerable files1) + public void DeleteFiles(Mod mod, IModDataContainer option, IEnumerable files1) { var deletions = 0; foreach (var file in files1) @@ -156,7 +156,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu } - private bool CheckAgainstMissing(Mod mod, SubMod option, FullPath file, Utf8GamePath key, bool removeUsed) + private bool CheckAgainstMissing(Mod mod, IModDataContainer option, FullPath file, Utf8GamePath key, bool removeUsed) { if (!files.Missing.Contains(file)) return true; diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 74e9007c..541c84ae 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -1,6 +1,5 @@ using Dalamud.Interface.Internal.Notifications; using Dalamud.Utility; -using ImGuizmoNET; using OtterGui; using OtterGui.Classes; using Penumbra.Api.Enums; @@ -33,9 +32,9 @@ public class ModMerger : IDisposable private readonly Dictionary _fileToFile = []; private readonly HashSet _createdDirectories = []; private readonly HashSet _createdGroups = []; - private readonly HashSet _createdOptions = []; + private readonly HashSet _createdOptions = []; - public readonly HashSet SelectedOptions = []; + public readonly HashSet SelectedOptions = []; public readonly IReadOnlyList Warnings = []; public Exception? Error { get; private set; } @@ -94,7 +93,7 @@ public class ModMerger : IDisposable private void MergeWithOptions() { - MergeIntoOption(Enumerable.Repeat(MergeFromMod!.Default, 1), MergeToMod!.Default, false); + MergeIntoOption([MergeFromMod!.Default], MergeToMod!.Default, false); foreach (var originalGroup in MergeFromMod!.Groups) { @@ -105,20 +104,13 @@ 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}."); - var optionEnumerator = group switch + foreach (var originalOption in group.DataContainers) { - 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); + var (option, _, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, originalOption.GetName()); if (optionCreated) { - _createdOptions.Add(option); - MergeIntoOption(Enumerable.Repeat(originalOption, 1), option, false); + _createdOptions.Add((IModDataOption)option); + MergeIntoOption([originalOption], (IModDataOption)option, false); } else { @@ -136,7 +128,7 @@ public class ModMerger : IDisposable if (groupName.Length == 0 && optionName.Length == 0) { CopyFiles(MergeToMod!.ModPath); - MergeIntoOption(MergeFromMod!.AllSubMods.Reverse(), MergeToMod!.Default, true); + MergeIntoOption(MergeFromMod!.AllDataContainers.Reverse(), MergeToMod!.Default, true); } else if (groupName.Length * optionName.Length == 0) { @@ -148,7 +140,7 @@ public class ModMerger : IDisposable _createdGroups.Add(groupIdx); var (option, _, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, optionName, SaveType.None); if (optionCreated) - _createdOptions.Add(option); + _createdOptions.Add((IModDataOption)option); var dir = ModCreator.NewOptionDirectory(MergeToMod!.ModPath, groupName, _config.ReplaceNonAsciiOnImport); if (!dir.Exists) _createdDirectories.Add(dir.FullName); @@ -156,14 +148,14 @@ public class ModMerger : IDisposable if (!dir.Exists) _createdDirectories.Add(dir.FullName); CopyFiles(dir); - MergeIntoOption(MergeFromMod!.AllSubMods.Reverse(), option, true); + MergeIntoOption(MergeFromMod!.AllDataContainers.Reverse(), (IModDataOption)option, true); } - private void MergeIntoOption(IEnumerable mergeOptions, SubMod option, bool fromFileToFile) + private void MergeIntoOption(IEnumerable mergeOptions, IModDataContainer option, bool fromFileToFile) { - var redirections = option.FileData.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - var swaps = option.FileSwapData.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - var manips = option.ManipulationData.ToHashSet(); + var redirections = option.Files.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + var swaps = option.FileSwaps.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + var manips = option.Manipulations.ToHashSet(); foreach (var originalOption in mergeOptions) { @@ -171,31 +163,31 @@ public class ModMerger : IDisposable { if (!manips.Add(manip)) throw new Exception( - $"Could not add meta manipulation {manip} from {originalOption.FullName} to {option.FullName} because another manipulation of the same data already exists in this option."); + $"Could not add meta manipulation {manip} from {originalOption.GetFullName()} to {option.GetFullName()} because another manipulation of the same data already exists in this option."); } foreach (var (swapA, swapB) in originalOption.FileSwaps) { if (!swaps.TryAdd(swapA, swapB)) throw new Exception( - $"Could not add file swap {swapB} -> {swapA} from {originalOption.FullName} to {option.FullName} because another swap of the key already exists."); + $"Could not add file swap {swapB} -> {swapA} from {originalOption.GetFullName()} to {option.GetFullName()} because another swap of the key already exists."); } foreach (var (gamePath, path) in originalOption.Files) { if (!GetFullPath(path, out var newFile)) throw new Exception( - $"Could not add file redirection {path} -> {gamePath} from {originalOption.FullName} to {option.FullName} because the file does not exist in the new mod."); + $"Could not add file redirection {path} -> {gamePath} from {originalOption.GetFullName()} to {option.GetFullName()} because the file does not exist in the new mod."); if (!redirections.TryAdd(gamePath, newFile)) throw new Exception( - $"Could not add file redirection {path} -> {gamePath} from {originalOption.FullName} to {option.FullName} because a redirection for the game path already exists."); + $"Could not add file redirection {path} -> {gamePath} from {originalOption.GetFullName()} to {option.GetFullName()} because a redirection for the game path already exists."); } } - 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); + var (groupIdx, dataIdx) = option.GetDataIndices(); + _editor.OptionSetFiles(MergeToMod!, groupIdx, dataIdx, redirections, SaveType.None); + _editor.OptionSetFileSwaps(MergeToMod!, groupIdx, dataIdx, swaps, SaveType.None); + _editor.OptionSetManipulations(MergeToMod!, groupIdx, dataIdx, manips, SaveType.ImmediateSync); return; bool GetFullPath(FullPath input, out FullPath ret) @@ -270,30 +262,29 @@ public class ModMerger : IDisposable { var files = CopySubModFiles(mods[0], dir); _editor.OptionSetFiles(result, -1, 0, files); - _editor.OptionSetFileSwaps(result, -1, 0, mods[0].FileSwapData); - _editor.OptionSetManipulations(result, -1, 0, mods[0].ManipulationData); + _editor.OptionSetFileSwaps(result, -1, 0, mods[0].FileSwaps); + _editor.OptionSetManipulations(result, -1, 0, mods[0].Manipulations); } else { foreach (var originalOption in mods) { - if (originalOption.IsDefault) + if (originalOption.Group is not {} originalGroup) { var files = CopySubModFiles(mods[0], dir); _editor.OptionSetFiles(result, -1, 0, files); - _editor.OptionSetFileSwaps(result, -1, 0, mods[0].FileSwapData); - _editor.OptionSetManipulations(result, -1, 0, mods[0].ManipulationData); + _editor.OptionSetFileSwaps(result, -1, 0, mods[0].FileSwaps); + _editor.OptionSetManipulations(result, -1, 0, mods[0].Manipulations); } else { - var originalGroup = originalOption.Group; - var (group, groupIdx, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name); - var (option, optionIdx, _) = _editor.FindOrAddOption(result, groupIdx, originalOption.Name); + var (group, groupIdx, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name); + var (option, optionIdx, _) = _editor.FindOrAddOption(result, groupIdx, originalOption.GetName()); var folder = Path.Combine(dir.FullName, group.Name, option.Name); var files = CopySubModFiles(originalOption, new DirectoryInfo(folder)); _editor.OptionSetFiles(result, groupIdx, optionIdx, files); - _editor.OptionSetFileSwaps(result, groupIdx, optionIdx, originalOption.FileSwapData); - _editor.OptionSetManipulations(result, groupIdx, optionIdx, originalOption.ManipulationData); + _editor.OptionSetFileSwaps(result, groupIdx, optionIdx, originalOption.FileSwaps); + _editor.OptionSetManipulations(result, groupIdx, optionIdx, originalOption.Manipulations); } } } @@ -315,11 +306,11 @@ public class ModMerger : IDisposable } } - private static Dictionary CopySubModFiles(SubMod option, DirectoryInfo newMod) + private static Dictionary CopySubModFiles(IModDataContainer option, DirectoryInfo newMod) { - var ret = new Dictionary(option.FileData.Count); + var ret = new Dictionary(option.Files.Count); var parentPath = ((Mod)option.Mod).ModPath.FullName; - foreach (var (path, file) in option.FileData) + foreach (var (path, file) in option.Files) { var target = Path.GetRelativePath(parentPath, file.FullName); target = Path.Combine(newMod.FullName, target); @@ -348,7 +339,7 @@ public class ModMerger : IDisposable { foreach (var option in _createdOptions) { - var (groupIdx, optionIdx) = option.GetIndices(); + var (groupIdx, optionIdx) = option.GetOptionIndices(); _editor.DeleteOption(MergeToMod!, groupIdx, optionIdx); Penumbra.Log.Verbose($"[Merger] Removed option {option.FullName}."); } diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index a6218c6f..88e48f0f 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -103,7 +103,7 @@ public class ModMetaEditor(ModManager modManager) Changes = true; } - public void Load(Mod mod, SubMod currentOption) + public void Load(Mod mod, IModDataContainer currentOption) { OtherImcCount = 0; OtherEqpCount = 0; @@ -111,7 +111,7 @@ public class ModMetaEditor(ModManager modManager) OtherGmpCount = 0; OtherEstCount = 0; OtherRspCount = 0; - foreach (var option in mod.AllSubMods) + foreach (var option in mod.AllDataContainers) { if (option == currentOption) continue; diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index 9698fdcb..db00a1c7 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -1,3 +1,4 @@ +using System; using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Classes; @@ -168,27 +169,11 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) foreach (var (group, groupIdx) in Mod.Groups.WithIndex()) { var groupDir = ModCreator.CreateModFolder(directory, group.Name, _config.ReplaceNonAsciiOnImport, true); - switch (group) - { - 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([]); - - 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; - } + _redirections[groupIdx + 1].EnsureCapacity(group.DataContainers.Count); + for (var i = _redirections[groupIdx + 1].Count; i < group.DataContainers.Count; ++i) + _redirections[groupIdx + 1].Add([]); + foreach (var (data, dataIdx) in group.DataContainers.WithIndex()) + HandleSubMod(groupDir, data, _redirections[groupIdx + 1][dataIdx]); } return true; @@ -200,13 +185,14 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) return false; - void HandleSubMod(DirectoryInfo groupDir, SubMod option, Dictionary newDict) + void HandleSubMod(DirectoryInfo groupDir, IModDataContainer option, Dictionary newDict) { - var optionDir = ModCreator.CreateModFolder(groupDir, option.Name, _config.ReplaceNonAsciiOnImport, true); + var name = option.GetName(); + var optionDir = ModCreator.CreateModFolder(groupDir, name, _config.ReplaceNonAsciiOnImport, true); newDict.Clear(); - newDict.EnsureCapacity(option.FileData.Count); - foreach (var (gamePath, fullPath) in option.FileData) + newDict.EnsureCapacity(option.Files.Count); + foreach (var (gamePath, fullPath) in option.Files) { var relPath = new Utf8RelPath(gamePath).ToString(); var newFullPath = Path.Combine(optionDir.FullName, relPath); @@ -300,7 +286,7 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) _modManager.OptionEditor.OptionSetFiles(Mod, groupIdx, optionIdx, _redirections[groupIdx + 1][optionIdx]); break; case MultiModGroup multi: - foreach (var (_, optionIdx) in multi.PrioritizedOptions.WithIndex()) + foreach (var (_, optionIdx) in multi.OptionData.WithIndex()) _modManager.OptionEditor.OptionSetFiles(Mod, groupIdx, optionIdx, _redirections[groupIdx + 1][optionIdx]); break; } diff --git a/Penumbra/Mods/Editor/ModSwapEditor.cs b/Penumbra/Mods/Editor/ModSwapEditor.cs index 0d5f05a9..64788cf3 100644 --- a/Penumbra/Mods/Editor/ModSwapEditor.cs +++ b/Penumbra/Mods/Editor/ModSwapEditor.cs @@ -11,7 +11,7 @@ public class ModSwapEditor(ModManager modManager) public IReadOnlyDictionary Swaps => _swaps; - public void Revert(SubMod option) + public void Revert(IModDataContainer option) { _swaps.SetTo(option.FileSwaps); Changes = false; diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index df243781..4f9e8648 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -176,13 +176,13 @@ public class ModCacheManager : IDisposable } private static void UpdateFileCount(Mod mod) - => mod.TotalFileCount = mod.AllSubMods.Sum(s => s.Files.Count); + => mod.TotalFileCount = mod.AllDataContainers.Sum(s => s.Files.Count); private static void UpdateSwapCount(Mod mod) - => mod.TotalSwapCount = mod.AllSubMods.Sum(s => s.FileSwaps.Count); + => mod.TotalSwapCount = mod.AllDataContainers.Sum(s => s.FileSwaps.Count); private static void UpdateMetaCount(Mod mod) - => mod.TotalManipulations = mod.AllSubMods.Sum(s => s.Manipulations.Count); + => mod.TotalManipulations = mod.AllDataContainers.Sum(s => s.Manipulations.Count); private static void UpdateHasOptions(Mod mod) => mod.HasOptions = mod.Groups.Any(o => o.IsOption); @@ -194,10 +194,10 @@ public class ModCacheManager : IDisposable { var changedItems = (SortedList)mod.ChangedItems; changedItems.Clear(); - foreach (var gamePath in mod.AllSubMods.SelectMany(m => m.Files.Keys.Concat(m.FileSwaps.Keys))) + foreach (var gamePath in mod.AllDataContainers.SelectMany(m => m.Files.Keys.Concat(m.FileSwaps.Keys))) _identifier.Identify(changedItems, gamePath.ToString()); - foreach (var manip in mod.AllSubMods.SelectMany(m => m.Manipulations)) + foreach (var manip in mod.AllDataContainers.SelectMany(m => m.Manipulations)) ComputeChangedItems(_identifier, changedItems, manip); mod.LowerChangedItemsString = string.Join("\0", mod.ChangedItems.Keys.Select(k => k.ToLowerInvariant())); @@ -211,20 +211,11 @@ public class ModCacheManager : IDisposable mod.HasOptions = false; foreach (var group in mod.Groups) { - mod.HasOptions |= group.IsOption; - 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; - mod.TotalManipulations += s.Manipulations.Count; - } + mod.HasOptions |= group.IsOption; + var (files, swaps, manips) = group.GetCounts(); + mod.TotalFileCount += files; + mod.TotalSwapCount += swaps; + mod.TotalManipulations += manips; } } diff --git a/Penumbra/Mods/Manager/ModMigration.cs b/Penumbra/Mods/Manager/ModMigration.cs index 9c8ced89..8c4a5674 100644 --- a/Penumbra/Mods/Manager/ModMigration.cs +++ b/Penumbra/Mods/Manager/ModMigration.cs @@ -71,14 +71,14 @@ public static partial class ModMigration foreach (var unusedFile in mod.FindUnusedFiles().Where(f => !seenMetaFiles.Contains(f))) { if (unusedFile.ToGamePath(mod.ModPath, out var gamePath) - && !mod.Default.FileData.TryAdd(gamePath, unusedFile)) - Penumbra.Log.Error($"Could not add {gamePath} because it already points to {mod.Default.FileData[gamePath]}."); + && !mod.Default.Files.TryAdd(gamePath, unusedFile)) + Penumbra.Log.Error($"Could not add {gamePath} because it already points to {mod.Default.Files[gamePath]}."); } - mod.Default.FileSwapData.Clear(); - mod.Default.FileSwapData.EnsureCapacity(swaps.Count); + mod.Default.FileSwaps.Clear(); + mod.Default.FileSwaps.EnsureCapacity(swaps.Count); foreach (var (gamePath, swapPath) in swaps) - mod.Default.FileSwapData.Add(gamePath, swapPath); + mod.Default.FileSwaps.Add(gamePath, swapPath); creator.IncorporateMetaChanges(mod.Default, mod.ModPath, true); foreach (var (_, index) in mod.Groups.WithIndex()) @@ -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, newMultiGroup, option, seenMetaFiles), optionPriority++)); + newMultiGroup.OptionData.Add(SubModFromOption(creator, mod, newMultiGroup, option, optionPriority++, seenMetaFiles)); break; case GroupType.Single: @@ -158,22 +158,41 @@ public static partial class ModMigration } } - private static void AddFilesToSubMod(SubMod mod, DirectoryInfo basePath, OptionV0 option, HashSet seenMetaFiles) + private static void AddFilesToSubMod(IModDataContainer mod, DirectoryInfo basePath, OptionV0 option, HashSet seenMetaFiles) { foreach (var (relPath, gamePaths) in option.OptionFiles) { var fullPath = new FullPath(basePath, relPath); foreach (var gamePath in gamePaths) - mod.FileData.TryAdd(gamePath, fullPath); + mod.Files.TryAdd(gamePath, fullPath); if (fullPath.Extension is ".meta" or ".rgsp") seenMetaFiles.Add(fullPath); } } - private static SubMod SubModFromOption(ModCreator creator, Mod mod, IModGroup group, OptionV0 option, HashSet seenMetaFiles) + private static SingleSubMod SubModFromOption(ModCreator creator, Mod mod, SingleModGroup group, OptionV0 option, + HashSet seenMetaFiles) { - var subMod = new SubMod(mod, group) { Name = option.OptionName }; + var subMod = new SingleSubMod(mod, group) + { + Name = option.OptionName, + Description = option.OptionDesc, + }; + AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles); + creator.IncorporateMetaChanges(subMod, mod.ModPath, false); + return subMod; + } + + private static MultiSubMod SubModFromOption(ModCreator creator, Mod mod, MultiModGroup group, OptionV0 option, + ModPriority priority, HashSet seenMetaFiles) + { + var subMod = new MultiSubMod(mod, group) + { + Name = option.OptionName, + Description = option.OptionDesc, + Priority = priority, + }; 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 e78b6209..4d3a5717 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -1,4 +1,3 @@ -using System.Security.AccessControl; using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Classes; @@ -179,10 +178,10 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS switch (mod.Groups[groupIdx]) { case MultiModGroup multi: - if (multi.PrioritizedOptions[optionIdx].Priority == newPriority) + if (multi.OptionData[optionIdx].Priority == newPriority) return; - multi.PrioritizedOptions[optionIdx] = (multi.PrioritizedOptions[optionIdx].Mod, newPriority); + multi.OptionData[optionIdx].Priority = newPriority; saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1); return; @@ -213,70 +212,62 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS } /// Add a new empty option of the given name for the given group if it does not exist already. - public (SubMod, int, bool) FindOrAddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue) + public (IModOption, int, bool) FindOrAddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue) { var group = mod.Groups[groupIdx]; - switch (group) - { - case SingleModGroup single: - { - var idx = single.OptionData.IndexOf(o => o.Name == newName); - if (idx >= 0) - return (single.OptionData[idx], idx, false); + var idx = group.Options.IndexOf(o => o.Name == newName); + if (idx >= 0) + return (group.Options[idx], idx, false); - idx = single.AddOption(mod, newName); - if (idx < 0) - throw new Exception($"Could not create new option with name {newName} in {group.Name}."); + idx = group.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 (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()}."); + saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1); + return (group.Options[idx], idx, true); } - /// Add an existing option to a given group with default priority. - public void AddOption(Mod mod, int groupIdx, SubMod option) - => AddOption(mod, groupIdx, option, ModPriority.Default); - - /// Add an existing option to a given group with a given priority. - public void AddOption(Mod mod, int groupIdx, SubMod option, ModPriority priority) + /// Add an existing option to a given group. + public void AddOption(Mod mod, int groupIdx, IModOption option) { var group = mod.Groups[groupIdx]; int idx; switch (group) { - case MultiModGroup { PrioritizedOptions.Count: >= IModGroup.MaxMultiOptions }: + case MultiModGroup { OptionData.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: + { idx = s.OptionData.Count; - s.OptionData.Add(option); + var newOption = new SingleSubMod(s.Mod, s) + { + Name = option.Name, + Description = option.Description, + }; + if (option is IModDataContainer data) + IModDataContainer.Clone(data, newOption); + s.OptionData.Add(newOption); break; + } case MultiModGroup m: - idx = m.PrioritizedOptions.Count; - m.PrioritizedOptions.Add((option, priority)); + { + idx = m.OptionData.Count; + var newOption = new MultiSubMod(m.Mod, m) + { + Name = option.Name, + Description = option.Description, + Priority = option is MultiSubMod s ? s.Priority : ModPriority.Default, + }; + if (option is IModDataContainer data) + IModDataContainer.Clone(data, newOption); + m.OptionData.Add(newOption); break; - default: - return; + } + default: return; } saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); @@ -295,7 +286,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS break; case MultiModGroup m: - m.PrioritizedOptions.RemoveAt(optionIdx); + m.OptionData.RemoveAt(optionIdx); break; } @@ -315,59 +306,59 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS } /// Set the meta manipulations for a given option. Replaces existing manipulations. - public void OptionSetManipulations(Mod mod, int groupIdx, int optionIdx, HashSet manipulations, + public void OptionSetManipulations(Mod mod, int groupIdx, int dataContainerIdx, HashSet manipulations, SaveType saveType = SaveType.Queue) { - var subMod = GetSubMod(mod, groupIdx, optionIdx); + var subMod = GetSubMod(mod, groupIdx, dataContainerIdx); if (subMod.Manipulations.Count == manipulations.Count && subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m))) return; - communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); - subMod.ManipulationData.SetTo(manipulations); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, dataContainerIdx, -1); + subMod.Manipulations.SetTo(manipulations); saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, dataContainerIdx, -1); } /// Set the file redirections for a given option. Replaces existing redirections. - public void OptionSetFiles(Mod mod, int groupIdx, int optionIdx, IReadOnlyDictionary replacements, + public void OptionSetFiles(Mod mod, int groupIdx, int dataContainerIdx, IReadOnlyDictionary replacements, SaveType saveType = SaveType.Queue) { - var subMod = GetSubMod(mod, groupIdx, optionIdx); - if (subMod.FileData.SetEquals(replacements)) + var subMod = GetSubMod(mod, groupIdx, dataContainerIdx); + if (subMod.Files.SetEquals(replacements)) return; - communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); - subMod.FileData.SetTo(replacements); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, dataContainerIdx, -1); + subMod.Files.SetTo(replacements); saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, dataContainerIdx, -1); } /// Add additional file redirections to a given option, keeping already existing ones. Only fires an event if anything is actually added. - public void OptionAddFiles(Mod mod, int groupIdx, int optionIdx, IReadOnlyDictionary additions) + public void OptionAddFiles(Mod mod, int groupIdx, int dataContainerIdx, IReadOnlyDictionary additions) { - var subMod = GetSubMod(mod, groupIdx, optionIdx); - var oldCount = subMod.FileData.Count; - subMod.FileData.AddFrom(additions); - if (oldCount != subMod.FileData.Count) + var subMod = GetSubMod(mod, groupIdx, dataContainerIdx); + var oldCount = subMod.Files.Count; + subMod.Files.AddFrom(additions); + if (oldCount != subMod.Files.Count) { saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, optionIdx, -1); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, dataContainerIdx, -1); } } /// Set the file swaps for a given option. Replaces existing swaps. - public void OptionSetFileSwaps(Mod mod, int groupIdx, int optionIdx, IReadOnlyDictionary swaps, + public void OptionSetFileSwaps(Mod mod, int groupIdx, int dataContainerIdx, IReadOnlyDictionary swaps, SaveType saveType = SaveType.Queue) { - var subMod = GetSubMod(mod, groupIdx, optionIdx); - if (subMod.FileSwapData.SetEquals(swaps)) + var subMod = GetSubMod(mod, groupIdx, dataContainerIdx); + if (subMod.FileSwaps.SetEquals(swaps)) return; - communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); - subMod.FileSwapData.SetTo(swaps); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, dataContainerIdx, -1); + subMod.FileSwaps.SetTo(swaps); saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1); + communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, dataContainerIdx, -1); } @@ -389,17 +380,12 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS } /// Get the correct option for the given group and option index. - private static SubMod GetSubMod(Mod mod, int groupIdx, int optionIdx) + private static IModDataContainer GetSubMod(Mod mod, int groupIdx, int dataContainerIdx) { - if (groupIdx == -1 && optionIdx == 0) + if (groupIdx == -1 && dataContainerIdx == 0) return mod.Default; - return mod.Groups[groupIdx] switch - { - SingleModGroup s => s.OptionData[optionIdx], - MultiModGroup m => m.PrioritizedOptions[optionIdx].Mod, - _ => throw new InvalidOperationException(), - }; + return mod.Groups[groupIdx].DataContainers[dataContainerIdx]; } } diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 71f64205..5c02213e 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 = SubMod.CreateDefault(this); + Default = new DefaultSubMod(this); } public override string ToString() @@ -61,8 +61,8 @@ public sealed class Mod : IMod // Options - public readonly SubMod Default; - public readonly List Groups = []; + public readonly DefaultSubMod Default; + public readonly List Groups = []; public AppliedModData GetData(ModSettings? settings = null) { @@ -77,21 +77,16 @@ public sealed class Mod : IMod group.AddData(config, dictRedirections, setManips); } - Default.AddData(dictRedirections, setManips); + Default.AddDataTo(dictRedirections, setManips); return new AppliedModData(dictRedirections, setManips); } - public IEnumerable AllSubMods - => Groups.SelectMany(o => o switch - { - SingleModGroup single => single.OptionData, - MultiModGroup multi => multi.PrioritizedOptions.Select(s => s.Mod), - _ => [], - }).Prepend(Default); + public IEnumerable AllDataContainers + => Groups.SelectMany(o => o.DataContainers).Prepend(Default); public List FindUnusedFiles() { - var modFiles = AllSubMods.SelectMany(o => o.Files) + var modFiles = AllDataContainers.SelectMany(o => o.Files) .Select(p => p.Value) .ToHashSet(); return ModPath.EnumerateDirectories() diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 4d32f395..c1236037 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -112,10 +112,8 @@ public partial class ModCreator( var defaultFile = _saveService.FileNames.OptionGroupFile(mod, -1, Config.ReplaceNonAsciiOnImport); try { - if (!File.Exists(defaultFile)) - mod.Default.Load(mod.ModPath, new JObject(), out _); - else - mod.Default.Load(mod.ModPath, JObject.Parse(File.ReadAllText(defaultFile)), out _); + var jObject = File.Exists(defaultFile) ? JObject.Parse(File.ReadAllText(defaultFile)) : new JObject(); + IModDataContainer.Load(jObject, mod.Default, mod.ModPath); } catch (Exception e) { @@ -154,7 +152,7 @@ public partial class ModCreator( { var changes = false; List deleteList = new(); - foreach (var subMod in mod.AllSubMods) + foreach (var subMod in mod.AllDataContainers) { var (localChanges, localDeleteList) = IncorporateMetaChanges(subMod, mod.ModPath, false); changes |= localChanges; @@ -162,7 +160,7 @@ public partial class ModCreator( deleteList.AddRange(localDeleteList); } - SubMod.DeleteDeleteList(deleteList, delete); + IModDataContainer.DeleteDeleteList(deleteList, delete); if (!changes) return; @@ -176,10 +174,10 @@ public partial class ModCreator( /// If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod. /// If delete is true, the files are deleted afterwards. /// - public (bool Changes, List DeleteList) IncorporateMetaChanges(SubMod option, DirectoryInfo basePath, bool delete) + public (bool Changes, List DeleteList) IncorporateMetaChanges(IModDataContainer option, DirectoryInfo basePath, bool delete) { var deleteList = new List(); - var oldSize = option.ManipulationData.Count; + var oldSize = option.Manipulations.Count; var deleteString = delete ? "with deletion." : "without deletion."; foreach (var (key, file) in option.Files.ToList()) { @@ -189,7 +187,7 @@ public partial class ModCreator( { if (ext1 == ".meta" || ext2 == ".meta") { - option.FileData.Remove(key); + option.Files.Remove(key); if (!file.Exists) continue; @@ -198,11 +196,11 @@ public partial class ModCreator( Penumbra.Log.Verbose( $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}"); deleteList.Add(file.FullName); - option.ManipulationData.UnionWith(meta.MetaManipulations); + option.Manipulations.UnionWith(meta.MetaManipulations); } else if (ext1 == ".rgsp" || ext2 == ".rgsp") { - option.FileData.Remove(key); + option.Files.Remove(key); if (!file.Exists) continue; @@ -212,7 +210,7 @@ public partial class ModCreator( $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}"); deleteList.Add(file.FullName); - option.ManipulationData.UnionWith(rgsp.MetaManipulations); + option.Manipulations.UnionWith(rgsp.MetaManipulations); } } catch (Exception e) @@ -221,8 +219,8 @@ public partial class ModCreator( } } - SubMod.DeleteDeleteList(deleteList, delete); - return (oldSize < option.ManipulationData.Count, deleteList); + IModDataContainer.DeleteDeleteList(deleteList, delete); + return (oldSize < option.Manipulations.Count, deleteList); } /// @@ -238,7 +236,7 @@ public partial class ModCreator( /// Create a file for an option group from given data. public void CreateOptionGroup(DirectoryInfo baseFolder, GroupType type, string name, - ModPriority priority, int index, Setting defaultSettings, string desc, IEnumerable subMods) + ModPriority priority, int index, Setting defaultSettings, string desc, IEnumerable subMods) { switch (type) { @@ -248,7 +246,7 @@ public partial class ModCreator( group.Description = desc; group.Priority = priority; group.DefaultSettings = defaultSettings; - group.PrioritizedOptions.AddRange(subMods.Select((s, idx) => (s, new ModPriority(idx)))); + group.OptionData.AddRange(subMods.Select(s => s.Clone(null!, group))); _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); break; } @@ -258,7 +256,7 @@ public partial class ModCreator( group.Description = desc; group.Priority = priority; group.DefaultSettings = defaultSettings; - group.OptionData.AddRange(subMods); + group.OptionData.AddRange(subMods.Select(s => s.ConvertToSingle(null!, group))); _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); break; } @@ -266,16 +264,15 @@ public partial class ModCreator( } /// Create the data for a given sub mod from its data and the folder it is based on. - public SubMod CreateSubMod(DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option) + public MultiSubMod CreateSubMod(DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option, ModPriority priority) { var list = optionFolder.EnumerateNonHiddenFiles() .Select(f => (Utf8GamePath.FromFile(f, optionFolder, out var gamePath, true), gamePath, new FullPath(f))) .Where(t => t.Item1); - var mod = SubMod.CreateForSaving(option.Name); - mod.Description = option.Description; + var mod = MultiSubMod.CreateForSaving(option.Name, option.Description, priority); foreach (var (_, gamePath, file) in list) - mod.FileData.TryAdd(gamePath, file); + mod.Files.TryAdd(gamePath, file); IncorporateMetaChanges(mod, baseFolder, true); return mod; @@ -292,7 +289,7 @@ public partial class ModCreator( foreach (var file in mod.FindUnusedFiles()) { if (Utf8GamePath.FromFile(new FileInfo(file.FullName), directory, out var gamePath, true)) - mod.Default.FileData.TryAdd(gamePath, file); + mod.Default.Files.TryAdd(gamePath, file); } IncorporateMetaChanges(mod.Default, directory, true); diff --git a/Penumbra/Mods/Subclasses/IModDataContainer.cs b/Penumbra/Mods/Subclasses/IModDataContainer.cs index d0b444b8..a26beb2a 100644 --- a/Penumbra/Mods/Subclasses/IModDataContainer.cs +++ b/Penumbra/Mods/Subclasses/IModDataContainer.cs @@ -1,12 +1,16 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; public interface IModDataContainer { + public IMod Mod { get; } + public IModGroup? Group { get; } + public Dictionary Files { get; set; } public Dictionary FileSwaps { get; set; } public HashSet Manipulations { get; set; } @@ -21,6 +25,32 @@ public interface IModDataContainer manipulations.UnionWith(Manipulations); } + public string GetName() + => this switch + { + IModOption o => o.FullName, + DefaultSubMod => DefaultSubMod.FullName, + _ => $"Container {GetDataIndices().DataIndex + 1}", + }; + + public string GetFullName() + => this switch + { + IModOption o => o.FullName, + DefaultSubMod => DefaultSubMod.FullName, + _ when Group != null => $"{Group.Name}: Container {GetDataIndices().DataIndex + 1}", + _ => $"Container {GetDataIndices().DataIndex + 1}", + }; + + public static void Clone(IModDataContainer from, IModDataContainer to) + { + to.Files = new Dictionary(from.Files); + to.FileSwaps = new Dictionary(from.FileSwaps); + to.Manipulations = [.. from.Manipulations]; + } + + public (int GroupIndex, int DataIndex) GetDataIndices(); + public static void Load(JToken json, IModDataContainer data, DirectoryInfo basePath) { data.Files.Clear(); @@ -77,4 +107,22 @@ public interface IModDataContainer serializer.Serialize(j, data.Manipulations); j.WriteEndObject(); } + + internal static void DeleteDeleteList(IEnumerable deleteList, bool delete) + { + if (!delete) + return; + + foreach (var file in deleteList) + { + try + { + File.Delete(file); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not delete incorporated meta file {file}:\n{e}"); + } + } + } } diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index a046ade0..5c500793 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -1,3 +1,4 @@ +using FFXIVClientStructs.FFXIV.Client.UI.Agent; using Newtonsoft.Json; using Penumbra.Api.Enums; using Penumbra.Meta.Manipulations; @@ -6,6 +7,11 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; +public interface ITexToolsGroup +{ + public IReadOnlyList OptionData { get; } +} + public interface IModGroup { public const int MaxMultiOptions = 63; @@ -19,28 +25,89 @@ public interface IModGroup 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 IReadOnlyList Options { get; } - public bool IsOption { get; } + public IReadOnlyList Options { get; } + public IReadOnlyList DataContainers { get; } + public bool IsOption { get; } public IModGroup Convert(GroupType type); public bool MoveOption(int optionIdxFrom, int optionIdxTo); + public int GetIndex(); + public void AddData(Setting setting, Dictionary redirections, HashSet manipulations); /// Ensure that a value is valid for a group. public Setting FixSetting(Setting setting); + + public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null); + + public bool ChangeOptionDescription(int optionIndex, string newDescription) + { + if (optionIndex < 0 || optionIndex >= Options.Count) + return false; + + var option = Options[optionIndex]; + if (option.Description == newDescription) + return false; + + option.Description = newDescription; + return true; + } + + public bool ChangeOptionName(int optionIndex, string newName) + { + if (optionIndex < 0 || optionIndex >= Options.Count) + return false; + + var option = Options[optionIndex]; + if (option.Name == newName) + return false; + + option.Name = newName; + return true; + } + + public static void WriteJsonBase(JsonTextWriter jWriter, IModGroup group) + { + jWriter.WriteStartObject(); + jWriter.WritePropertyName(nameof(group.Name)); + jWriter.WriteValue(group!.Name); + jWriter.WritePropertyName(nameof(group.Description)); + jWriter.WriteValue(group.Description); + jWriter.WritePropertyName(nameof(group.Priority)); + jWriter.WriteValue(group.Priority.Value); + jWriter.WritePropertyName(nameof(group.Type)); + jWriter.WriteValue(group.Type.ToString()); + jWriter.WritePropertyName(nameof(group.DefaultSettings)); + jWriter.WriteValue(group.DefaultSettings.Value); + } + + public (int Redirections, int Swaps, int Manips) GetCounts(); + + public static (int Redirections, int Swaps, int Manips) GetCountsBase(IModGroup group) + { + var redirectionCount = 0; + var swapCount = 0; + var manipCount = 0; + foreach (var option in group.DataContainers) + { + redirectionCount += option.Files.Count; + swapCount += option.FileSwaps.Count; + manipCount += option.Manipulations.Count; + } + + return (redirectionCount, swapCount, manipCount); + } } public readonly struct ModSaveGroup : ISavable { - private readonly DirectoryInfo _basePath; - private readonly IModGroup? _group; - private readonly int _groupIdx; - private readonly SubMod? _defaultMod; - private readonly bool _onlyAscii; + private readonly DirectoryInfo _basePath; + private readonly IModGroup? _group; + private readonly int _groupIdx; + private readonly DefaultSubMod? _defaultMod; + private readonly bool _onlyAscii; public ModSaveGroup(Mod mod, int groupIdx, bool onlyAscii) { @@ -61,7 +128,7 @@ public readonly struct ModSaveGroup : ISavable _onlyAscii = onlyAscii; } - public ModSaveGroup(DirectoryInfo basePath, SubMod @default, bool onlyAscii) + public ModSaveGroup(DirectoryInfo basePath, DefaultSubMod @default, bool onlyAscii) { _basePath = basePath; _groupIdx = -1; @@ -77,42 +144,11 @@ public readonly struct ModSaveGroup : ISavable using var j = new JsonTextWriter(writer); j.Formatting = Formatting.Indented; var serializer = new JsonSerializer { Formatting = Formatting.Indented }; + j.WriteStartObject(); if (_groupIdx >= 0) - { - j.WriteStartObject(); - j.WritePropertyName(nameof(_group.Name)); - j.WriteValue(_group!.Name); - j.WritePropertyName(nameof(_group.Description)); - j.WriteValue(_group.Description); - j.WritePropertyName(nameof(_group.Priority)); - j.WriteValue(_group.Priority.Value); - j.WritePropertyName(nameof(Type)); - j.WriteValue(_group.Type.ToString()); - j.WritePropertyName(nameof(_group.DefaultSettings)); - j.WriteValue(_group.DefaultSettings.Value); - switch (_group) - { - case SingleModGroup single: - j.WritePropertyName("Options"); - j.WriteStartArray(); - foreach (var option in single.OptionData) - SubMod.WriteSubMod(j, serializer, option, _basePath, null); - j.WriteEndArray(); - j.WriteEndObject(); - break; - case MultiModGroup multi: - j.WritePropertyName("Options"); - j.WriteStartArray(); - foreach (var (option, priority) in multi.PrioritizedOptions) - SubMod.WriteSubMod(j, serializer, option, _basePath, priority); - j.WriteEndArray(); - j.WriteEndObject(); - break; - } - } + _group!.WriteJson(j, serializer); else - { - SubMod.WriteSubMod(j, serializer, _defaultMod!, _basePath, null); - } + IModDataContainer.WriteModData(j, serializer, _defaultMod!, _basePath); + j.WriteEndObject(); } } diff --git a/Penumbra/Mods/Subclasses/IModOption.cs b/Penumbra/Mods/Subclasses/IModOption.cs index bb52a2cd..f66ce44e 100644 --- a/Penumbra/Mods/Subclasses/IModOption.cs +++ b/Penumbra/Mods/Subclasses/IModOption.cs @@ -15,6 +15,8 @@ public interface IModOption option.Description = json[nameof(Description)]?.ToObject() ?? string.Empty; } + public (int GroupIndex, int OptionIndex) GetOptionIndices(); + public static void WriteModOption(JsonWriter j, IModOption option) { j.WritePropertyName(nameof(Name)); diff --git a/Penumbra/Mods/Subclasses/MultiModGroup.cs b/Penumbra/Mods/Subclasses/MultiModGroup.cs index 4ec2c72a..f194350a 100644 --- a/Penumbra/Mods/Subclasses/MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/MultiModGroup.cs @@ -1,4 +1,5 @@ using Dalamud.Interface.Internal.Notifications; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; @@ -10,20 +11,30 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; /// Groups that allow all available options to be selected at once. -public sealed class MultiModGroup(Mod mod) : IModGroup +public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup { 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; } - public Setting DefaultSettings { get; set; } + 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; } + public Setting DefaultSettings { get; set; } + public readonly List OptionData = []; + + public IReadOnlyList Options + => OptionData; + + public IReadOnlyList DataContainers + => OptionData; + + public bool IsOption + => OptionData.Count > 0; public FullPath? FindBestMatch(Utf8GamePath gamePath) - => PrioritizedOptions.OrderByDescending(o => o.Priority) - .SelectWhere(o => (o.Mod.FileData.TryGetValue(gamePath, out var file) || o.Mod.FileSwapData.TryGetValue(gamePath, out file), file)) + => OptionData.OrderByDescending(o => o.Priority) + .SelectWhere(o => (o.Files.TryGetValue(gamePath, out var file) || o.FileSwaps.TryGetValue(gamePath, out file), file)) .FirstOrDefault(); public int AddOption(Mod mod, string name, string description = "") @@ -32,49 +43,15 @@ public sealed class MultiModGroup(Mod mod) : IModGroup if (groupIdx < 0) return -1; - var subMod = new SubMod(mod, this) + var subMod = new MultiSubMod(mod, this) { Name = name, Description = description, }; - PrioritizedOptions.Add((subMod, ModPriority.Default)); - return PrioritizedOptions.Count - 1; + OptionData.Add(subMod); + return OptionData.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 - => PrioritizedOptions.Count > 0; - - public readonly List<(SubMod Mod, ModPriority Priority)> PrioritizedOptions = []; - public static MultiModGroup? Load(Mod mod, JObject json, int groupIdx) { var ret = new MultiModGroup(mod) @@ -91,7 +68,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup if (options != null) foreach (var child in options.Children()) { - if (ret.PrioritizedOptions.Count == IModGroup.MaxMultiOptions) + if (ret.OptionData.Count == IModGroup.MaxMultiOptions) { Penumbra.Messager.NotificationMessage( $"Multi Group {ret.Name} in {mod.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options.", @@ -99,9 +76,8 @@ public sealed class MultiModGroup(Mod mod) : IModGroup break; } - var subMod = new SubMod(mod, ret); - subMod.Load(mod.ModPath, child, out var priority); - ret.PrioritizedOptions.Add((subMod, priority)); + var subMod = new MultiSubMod(mod, ret, child); + ret.OptionData.Add(subMod); } ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings); @@ -115,39 +91,68 @@ public sealed class MultiModGroup(Mod mod) : IModGroup { case GroupType.Multi: return this; case GroupType.Single: - var multi = new SingleModGroup(Mod) + var single = new SingleModGroup(Mod) { Name = Name, Description = Description, Priority = Priority, - DefaultSettings = DefaultSettings.TurnMulti(PrioritizedOptions.Count), + DefaultSettings = DefaultSettings.TurnMulti(OptionData.Count), }; - multi.OptionData.AddRange(PrioritizedOptions.Select(p => p.Mod)); - return multi; + single.OptionData.AddRange(OptionData.Select(o => o.ConvertToSingle(Mod, single))); + return single; default: throw new ArgumentOutOfRangeException(nameof(type), type, null); } } public bool MoveOption(int optionIdxFrom, int optionIdxTo) { - if (!PrioritizedOptions.Move(optionIdxFrom, optionIdxTo)) + if (!OptionData.Move(optionIdxFrom, optionIdxTo)) return false; DefaultSettings = DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo); return true; } + public int GetIndex() + { + var groupIndex = Mod.Groups.IndexOf(this); + if (groupIndex < 0) + throw new Exception($"Mod {Mod.Name} from Group {Name} does not contain this group."); + + return groupIndex; + } + public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) { - foreach (var (option, index) in PrioritizedOptions.WithIndex().OrderByDescending(o => o.Value.Priority)) + foreach (var (option, index) in OptionData.WithIndex().OrderByDescending(o => o.Value.Priority)) { if (setting.HasFlag(index)) - option.Mod.AddData(redirections, manipulations); + option.AddDataTo(redirections, manipulations); } } + public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null) + { + IModGroup.WriteJsonBase(jWriter, this); + jWriter.WritePropertyName("Options"); + jWriter.WriteStartArray(); + foreach (var option in OptionData) + { + IModOption.WriteModOption(jWriter, option); + jWriter.WritePropertyName(nameof(option.Priority)); + jWriter.WriteValue(option.Priority.Value); + IModDataContainer.WriteModData(jWriter, serializer, option, basePath ?? Mod.ModPath); + } + + jWriter.WriteEndArray(); + jWriter.WriteEndObject(); + } + + public (int Redirections, int Swaps, int Manips) GetCounts() + => IModGroup.GetCountsBase(this); + public Setting FixSetting(Setting setting) - => new(setting.Value & ((1ul << PrioritizedOptions.Count) - 1)); + => new(setting.Value & ((1ul << OptionData.Count) - 1)); /// Create a group without a mod only for saving it in the creator. internal static MultiModGroup CreateForSaving(string name) @@ -155,4 +160,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup { Name = name, }; + + IReadOnlyList ITexToolsGroup.OptionData + => OptionData; } diff --git a/Penumbra/Mods/Subclasses/SingleModGroup.cs b/Penumbra/Mods/Subclasses/SingleModGroup.cs index 994a1f96..d1a3b6d1 100644 --- a/Penumbra/Mods/Subclasses/SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/SingleModGroup.cs @@ -1,3 +1,4 @@ +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Filesystem; @@ -8,7 +9,7 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; /// Groups that allow only one of their available options to be selected. -public sealed class SingleModGroup(Mod mod) : IModGroup +public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup { public GroupType Type => GroupType.Single; @@ -19,16 +20,19 @@ public sealed class SingleModGroup(Mod mod) : IModGroup public ModPriority Priority { get; set; } public Setting DefaultSettings { get; set; } - public readonly List OptionData = []; + public readonly List OptionData = []; + + IReadOnlyList ITexToolsGroup.OptionData + => OptionData; public FullPath? FindBestMatch(Utf8GamePath gamePath) => OptionData - .SelectWhere(m => (m.FileData.TryGetValue(gamePath, out var file) || m.FileSwapData.TryGetValue(gamePath, out file), file)) + .SelectWhere(m => (m.Files.TryGetValue(gamePath, out var file) || m.FileSwaps.TryGetValue(gamePath, out file), file)) .FirstOrDefault(); public int AddOption(Mod mod, string name, string description = "") { - var subMod = new SubMod(mod, this) + var subMod = new SingleSubMod(mod, this) { Name = name, Description = description, @@ -37,35 +41,12 @@ public sealed class SingleModGroup(Mod mod) : IModGroup 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 IReadOnlyList DataContainers + => OptionData; + public bool IsOption => OptionData.Count > 1; @@ -85,8 +66,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup if (options != null) foreach (var child in options.Children()) { - var subMod = new SubMod(mod, ret); - subMod.Load(mod.ModPath, child, out _); + var subMod = new SingleSubMod(mod, ret, child); ret.OptionData.Add(subMod); } @@ -107,7 +87,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup Priority = Priority, DefaultSettings = Setting.Multi((int)DefaultSettings.Value), }; - multi.PrioritizedOptions.AddRange(OptionData.Select((o, i) => (o, new ModPriority(i)))); + multi.OptionData.AddRange(OptionData.Select((o, i) => o.ConvertToMulti(Mod, multi, new ModPriority(i)))); return multi; default: throw new ArgumentOutOfRangeException(nameof(type), type, null); } @@ -137,12 +117,39 @@ public sealed class SingleModGroup(Mod mod) : IModGroup return true; } + public int GetIndex() + { + var groupIndex = Mod.Groups.IndexOf(this); + if (groupIndex < 0) + throw new Exception($"Mod {Mod.Name} from Group {Name} does not contain this group."); + + return groupIndex; + } + public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) - => OptionData[setting.AsIndex].AddData(redirections, manipulations); + => OptionData[setting.AsIndex].AddDataTo(redirections, manipulations); public Setting FixSetting(Setting setting) => OptionData.Count == 0 ? Setting.Zero : new Setting(Math.Min(setting.Value, (ulong)(OptionData.Count - 1))); + public (int Redirections, int Swaps, int Manips) GetCounts() + => IModGroup.GetCountsBase(this); + + public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null) + { + IModGroup.WriteJsonBase(jWriter, this); + jWriter.WritePropertyName("Options"); + jWriter.WriteStartArray(); + foreach (var option in OptionData) + { + IModOption.WriteModOption(jWriter, option); + IModDataContainer.WriteModData(jWriter, serializer, option, basePath ?? Mod.ModPath); + } + + jWriter.WriteEndArray(); + jWriter.WriteEndObject(); + } + /// Create a group without a mod only for saving it in the creator. internal static SingleModGroup CreateForSaving(string name) => new(null!) diff --git a/Penumbra/Mods/Subclasses/SubMod.cs b/Penumbra/Mods/Subclasses/SubMod.cs index bc93fcc4..a2425eb7 100644 --- a/Penumbra/Mods/Subclasses/SubMod.cs +++ b/Penumbra/Mods/Subclasses/SubMod.cs @@ -7,7 +7,9 @@ using Penumbra.String.Classes; namespace Penumbra.Mods.Subclasses; -public class SingleSubMod(Mod mod, SingleModGroup group) : IModOption, IModDataContainer +public interface IModDataOption : IModOption, IModDataContainer; + +public class SingleSubMod(Mod mod, SingleModGroup group) : IModDataOption { internal readonly Mod Mod = mod; internal readonly SingleModGroup Group = group; @@ -19,12 +21,68 @@ public class SingleSubMod(Mod mod, SingleModGroup group) : IModOption, IModDataC public string Description { get; set; } = string.Empty; + IMod IModDataContainer.Mod + => Mod; + + IModGroup IModDataContainer.Group + => Group; + public Dictionary Files { get; set; } = []; public Dictionary FileSwaps { get; set; } = []; public HashSet Manipulations { get; set; } = []; + + public SingleSubMod(Mod mod, SingleModGroup group, JToken json) + : this(mod, group) + { + IModOption.Load(json, this); + IModDataContainer.Load(json, this, mod.ModPath); + } + + public SingleSubMod Clone(Mod mod, SingleModGroup group) + { + var ret = new SingleSubMod(mod, group) + { + Name = Name, + Description = Description, + }; + IModDataContainer.Clone(this, ret); + + return ret; + } + + public MultiSubMod ConvertToMulti(Mod mod, MultiModGroup group, ModPriority priority) + { + var ret = new MultiSubMod(mod, group) + { + Name = Name, + Description = Description, + Priority = priority, + }; + IModDataContainer.Clone(this, ret); + + return ret; + } + + public void AddDataTo(Dictionary redirections, HashSet manipulations) + => ((IModDataContainer)this).AddDataTo(redirections, manipulations); + + public (int GroupIndex, int DataIndex) GetDataIndices() + => (Group.GetIndex(), GetDataIndex()); + + public (int GroupIndex, int OptionIndex) GetOptionIndices() + => (Group.GetIndex(), GetDataIndex()); + + private int GetDataIndex() + { + var dataIndex = Group.DataContainers.IndexOf(this); + if (dataIndex < 0) + throw new Exception($"Group {Group.Name} from SubMod {Name} does not contain this SubMod."); + + return dataIndex; + } } -public class MultiSubMod(Mod mod, MultiModGroup group) : IModOption, IModDataContainer +public class MultiSubMod(Mod mod, MultiModGroup group) : IModDataOption { internal readonly Mod Mod = mod; internal readonly MultiModGroup Group = group; @@ -40,12 +98,76 @@ public class MultiSubMod(Mod mod, MultiModGroup group) : IModOption, IModDataCon public Dictionary Files { get; set; } = []; public Dictionary FileSwaps { get; set; } = []; public HashSet Manipulations { get; set; } = []; + + IMod IModDataContainer.Mod + => Mod; + + IModGroup IModDataContainer.Group + => Group; + + + public MultiSubMod(Mod mod, MultiModGroup group, JToken json) + : this(mod, group) + { + IModOption.Load(json, this); + IModDataContainer.Load(json, this, mod.ModPath); + Priority = json[nameof(IModGroup.Priority)]?.ToObject() ?? ModPriority.Default; + } + + public MultiSubMod Clone(Mod mod, MultiModGroup group) + { + var ret = new MultiSubMod(mod, group) + { + Name = Name, + Description = Description, + Priority = Priority, + }; + IModDataContainer.Clone(this, ret); + + return ret; + } + + public SingleSubMod ConvertToSingle(Mod mod, SingleModGroup group) + { + var ret = new SingleSubMod(mod, group) + { + Name = Name, + Description = Description, + }; + IModDataContainer.Clone(this, ret); + return ret; + } + + public void AddDataTo(Dictionary redirections, HashSet manipulations) + => ((IModDataContainer)this).AddDataTo(redirections, manipulations); + + public static MultiSubMod CreateForSaving(string name, string description, ModPriority priority) + => new(null!, null!) + { + Name = name, + Description = description, + Priority = priority, + }; + + public (int GroupIndex, int DataIndex) GetDataIndices() + => (Group.GetIndex(), GetDataIndex()); + + public (int GroupIndex, int OptionIndex) GetOptionIndices() + => (Group.GetIndex(), GetDataIndex()); + + private int GetDataIndex() + { + var dataIndex = Group.DataContainers.IndexOf(this); + if (dataIndex < 0) + throw new Exception($"Group {Group.Name} from SubMod {Name} does not contain this SubMod."); + + return dataIndex; + } } public class DefaultSubMod(IMod mod) : IModDataContainer { - public string FullName - => "Default Option"; + public const string FullName = "Default Option"; public string Description => string.Empty; @@ -55,183 +177,176 @@ public class DefaultSubMod(IMod mod) : IModDataContainer public Dictionary Files { get; set; } = []; public Dictionary FileSwaps { get; set; } = []; public HashSet Manipulations { get; set; } = []; + + IMod IModDataContainer.Mod + => Mod; + + IModGroup? IModDataContainer.Group + => null; + + + public DefaultSubMod(Mod mod, JToken json) + : this(mod) + { + IModDataContainer.Load(json, this, mod.ModPath); + } + + public void AddDataTo(Dictionary redirections, HashSet manipulations) + => ((IModDataContainer)this).AddDataTo(redirections, manipulations); + + public (int GroupIndex, int DataIndex) GetDataIndices() + => (-1, 0); } -/// -/// A sub mod is a collection of -/// - file replacements -/// - file swaps -/// - meta manipulations -/// that can be used either as an option or as the default data for a mod. -/// It can be loaded and reloaded from Json. -/// 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(IMod mod, IModGroup group) : IModOption -{ - public string Name { get; set; } = "Default"; - public string FullName - => Group == null ? "Default Option" : $"{Group.Name}: {Name}"; - - public string Description { get; set; } = string.Empty; - - 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 - => Group == null; - - public void AddData(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 Dictionary FileData = []; - public Dictionary FileSwapData = []; - public HashSet ManipulationData = []; - - public IReadOnlyDictionary Files - => FileData; - - public IReadOnlyDictionary FileSwaps - => FileSwapData; - - public IReadOnlySet Manipulations - => ManipulationData; - - public void Load(DirectoryInfo basePath, JToken json, out ModPriority priority) - { - FileData.Clear(); - FileSwapData.Clear(); - ManipulationData.Clear(); - - // Every option has a name, but priorities are only relevant for multi group options. - Name = json[nameof(Name)]?.ToObject() ?? string.Empty; - Description = json[nameof(Description)]?.ToObject() ?? string.Empty; - priority = json[nameof(IModGroup.Priority)]?.ToObject() ?? ModPriority.Default; - - var files = (JObject?)json[nameof(Files)]; - if (files != null) - foreach (var property in files.Properties()) - { - if (Utf8GamePath.FromString(property.Name, out var p, true)) - FileData.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)) - FileSwapData.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())) - ManipulationData.Add(s); - } - - internal static void DeleteDeleteList(IEnumerable deleteList, bool delete) - { - if (!delete) - return; - - foreach (var file in deleteList) - { - try - { - File.Delete(file); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not delete incorporated meta file {file}:\n{e}"); - } - } - } - - /// 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(); - j.WritePropertyName(nameof(Name)); - j.WriteValue(mod.Name); - j.WritePropertyName(nameof(Description)); - j.WriteValue(mod.Description); - if (priority != null) - { - j.WritePropertyName(nameof(IModGroup.Priority)); - j.WriteValue(priority.Value.Value); - } - - j.WritePropertyName(nameof(mod.Files)); - j.WriteStartObject(); - foreach (var (gamePath, file) in mod.Files) - { - if (file.ToRelPath(basePath, out var relPath)) - { - j.WritePropertyName(gamePath.ToString()); - j.WriteValue(relPath.ToString()); - } - } - - j.WriteEndObject(); - j.WritePropertyName(nameof(mod.FileSwaps)); - j.WriteStartObject(); - foreach (var (gamePath, file) in mod.FileSwaps) - { - j.WritePropertyName(gamePath.ToString()); - j.WriteValue(file.ToString()); - } - - j.WriteEndObject(); - j.WritePropertyName(nameof(mod.Manipulations)); - serializer.Serialize(j, mod.Manipulations); - j.WriteEndObject(); - } -} +//public sealed class SubMod(IMod mod, IModGroup group) : IModOption +//{ +// public string Name { get; set; } = "Default"; +// +// public string FullName +// => Group == null ? "Default Option" : $"{Group.Name}: {Name}"; +// +// public string Description { get; set; } = string.Empty; +// +// 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.OptionData.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 +// => Group == null; +// +// public void AddData(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 Dictionary FileData = []; +// public Dictionary FileSwapData = []; +// public HashSet ManipulationData = []; +// +// public IReadOnlyDictionary Files +// => FileData; +// +// public IReadOnlyDictionary FileSwaps +// => FileSwapData; +// +// public IReadOnlySet Manipulations +// => ManipulationData; +// +// public void Load(DirectoryInfo basePath, JToken json, out ModPriority priority) +// { +// FileData.Clear(); +// FileSwapData.Clear(); +// ManipulationData.Clear(); +// +// // Every option has a name, but priorities are only relevant for multi group options. +// Name = json[nameof(Name)]?.ToObject() ?? string.Empty; +// Description = json[nameof(Description)]?.ToObject() ?? string.Empty; +// priority = json[nameof(IModGroup.Priority)]?.ToObject() ?? ModPriority.Default; +// +// var files = (JObject?)json[nameof(Files)]; +// if (files != null) +// foreach (var property in files.Properties()) +// { +// if (Utf8GamePath.FromString(property.Name, out var p, true)) +// FileData.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)) +// FileSwapData.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())) +// ManipulationData.Add(s); +// } +// +// +// /// 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(); +// j.WritePropertyName(nameof(Name)); +// j.WriteValue(mod.Name); +// j.WritePropertyName(nameof(Description)); +// j.WriteValue(mod.Description); +// if (priority != null) +// { +// j.WritePropertyName(nameof(IModGroup.Priority)); +// j.WriteValue(priority.Value.Value); +// } +// +// j.WritePropertyName(nameof(mod.Files)); +// j.WriteStartObject(); +// foreach (var (gamePath, file) in mod.Files) +// { +// if (file.ToRelPath(basePath, out var relPath)) +// { +// j.WritePropertyName(gamePath.ToString()); +// j.WriteValue(relPath.ToString()); +// } +// } +// +// j.WriteEndObject(); +// j.WritePropertyName(nameof(mod.FileSwaps)); +// j.WriteStartObject(); +// foreach (var (gamePath, file) in mod.FileSwaps) +// { +// j.WritePropertyName(gamePath.ToString()); +// j.WriteValue(file.ToString()); +// } +// +// j.WriteEndObject(); +// j.WritePropertyName(nameof(mod.Manipulations)); +// serializer.Serialize(j, mod.Manipulations); +// j.WriteEndObject(); +// } +//} diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index a599b3bb..393369d4 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -18,49 +18,46 @@ public class TemporaryMod : IMod public int TotalManipulations => Default.Manipulations.Count; - public readonly SubMod Default; + public readonly DefaultSubMod Default; public AppliedModData GetData(ModSettings? settings = null) { Dictionary dict; - if (Default.FileSwapData.Count == 0) + if (Default.FileSwaps.Count == 0) { - dict = Default.FileData; + dict = Default.Files; } - else if (Default.FileData.Count == 0) + else if (Default.Files.Count == 0) { - dict = Default.FileSwapData; + dict = Default.FileSwaps; } else { // Need to ensure uniqueness. - dict = new Dictionary(Default.FileData.Count + Default.FileSwaps.Count); - foreach (var (gamePath, file) in Default.FileData.Concat(Default.FileSwaps)) + dict = new Dictionary(Default.Files.Count + Default.FileSwaps.Count); + foreach (var (gamePath, file) in Default.Files.Concat(Default.FileSwaps)) dict.TryAdd(gamePath, file); } - return new AppliedModData(dict, Default.ManipulationData); + return new AppliedModData(dict, Default.Manipulations); } public IReadOnlyList Groups => Array.Empty(); - public IEnumerable AllSubMods - => [Default]; - public TemporaryMod() - => Default = SubMod.CreateDefault(this); + => Default = new(this); public void SetFile(Utf8GamePath gamePath, FullPath fullPath) - => Default.FileData[gamePath] = fullPath; + => Default.Files[gamePath] = fullPath; public bool SetManipulation(MetaManipulation manip) - => Default.ManipulationData.Remove(manip) | Default.ManipulationData.Add(manip); + => Default.Manipulations.Remove(manip) | Default.Manipulations.Add(manip); public void SetAll(Dictionary dict, HashSet manips) { - Default.FileData = dict; - Default.ManipulationData = manips; + Default.Files = dict; + Default.Manipulations = manips; } public static void SaveTempCollection(Configuration config, SaveService saveService, ModManager modManager, ModCollection collection, @@ -93,16 +90,16 @@ public class TemporaryMod : IMod { var target = Path.Combine(fileDir.FullName, Path.GetFileName(targetPath)); File.Copy(targetPath, target, true); - defaultMod.FileData[gamePath] = new FullPath(target); + defaultMod.Files[gamePath] = new FullPath(target); } else { - defaultMod.FileSwapData[gamePath] = new FullPath(targetPath); + defaultMod.FileSwaps[gamePath] = new FullPath(targetPath); } } foreach (var manip in collection.MetaCache?.Manipulations ?? Array.Empty()) - defaultMod.ManipulationData.Add(manip); + defaultMod.Manipulations.Add(manip); saveService.ImmediateSave(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport)); modManager.AddMod(dir); diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index c891d33a..503d64b7 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -305,7 +305,7 @@ public class FileEditor( UiHelpers.Text(gamePath.Path); ImGui.TableNextColumn(); using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value()); - ImGui.TextUnformatted(option.FullName); + ImGui.TextUnformatted(option.GetFullName()); } } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index f765b47e..94f1d577 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -3,7 +3,6 @@ using ImGuiNET; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; -using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Subclasses; using Penumbra.String.Classes; @@ -79,7 +78,7 @@ public partial class ModEditWindow var file = f.RelPath.ToString(); return f.SubModUsage.Count == 0 ? Enumerable.Repeat((file, "Unused", string.Empty, 0x40000080u), 1) - : f.SubModUsage.Select(s => (file, s.Item2.ToString(), s.Item1.FullName, + : f.SubModUsage.Select(s => (file, s.Item2.ToString(), s.Item1.GetFullName(), _editor.Option! == s.Item1 && Mod!.HasOptions ? 0x40008000u : 0u)); }); @@ -148,13 +147,13 @@ public partial class ModEditWindow (string, int) GetMulti() { var groups = registry.SubModUsage.GroupBy(s => s.Item1).ToArray(); - return (string.Join("\n", groups.Select(g => g.Key.Name)), groups.Length); + return (string.Join("\n", groups.Select(g => g.Key.GetName())), groups.Length); } var (text, groupCount) = color switch { ColorId.ConflictingMod => (string.Empty, 0), - ColorId.NewMod => (registry.SubModUsage[0].Item1.Name, 1), + ColorId.NewMod => (registry.SubModUsage[0].Item1.GetName(), 1), ColorId.InheritedMod => GetMulti(), _ => (string.Empty, 0), }; @@ -192,7 +191,7 @@ public partial class ModEditWindow ImGuiUtil.RightAlign(rightText); } - private void PrintGamePath(int i, int j, FileRegistry registry, SubMod subMod, Utf8GamePath gamePath) + private void PrintGamePath(int i, int j, FileRegistry registry, IModDataContainer subMod, Utf8GamePath gamePath) { using var id = ImRaii.PushId(j); ImGui.TableNextColumn(); @@ -228,7 +227,7 @@ public partial class ModEditWindow } } - private void PrintNewGamePath(int i, FileRegistry registry, SubMod subMod) + private void PrintNewGamePath(int i, FileRegistry registry, IModDataContainer subMod) { var tmp = _fileIdx == i && _pathIdx == -1 ? _gamePathEdit : string.Empty; var pos = ImGui.GetCursorPosX() - ImGui.GetFrameHeight(); @@ -301,9 +300,9 @@ public partial class ModEditWindow tt = changes ? "Apply the current file setup to the currently selected option." : "No changes made."; if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, !changes)) { - var failedFiles = _editor.FileEditor.Apply(_editor.Mod!, (SubMod)_editor.Option!); + var failedFiles = _editor.FileEditor.Apply(_editor.Mod!, _editor.Option!); if (failedFiles > 0) - Penumbra.Log.Information($"Failed to apply {failedFiles} file redirections to {_editor.Option!.FullName}."); + Penumbra.Log.Information($"Failed to apply {failedFiles} file redirections to {_editor.Option!.GetFullName()}."); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index aad70cb3..92a9dd66 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -45,7 +45,7 @@ public partial class ModEditWindow var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option."; ImGui.NewLine(); if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, setsEqual)) - _editor.MetaEditor.Apply(_editor.Mod!, _editor.GroupIdx, _editor.OptionIdx); + _editor.MetaEditor.Apply(_editor.Mod!, _editor.GroupIdx, _editor.DataIdx); ImGui.SameLine(); tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index cca8fe10..6b9965b8 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -85,7 +85,7 @@ public partial class ModEditWindow { // TODO: Is it worth trying to order results based on option priorities for cases where more than one match is found? // NOTE: We're using case-insensitive comparisons, as option group paths in mods are stored in lower case, but the mod editor uses paths directly from the file system, which may be mixed case. - return mod.AllSubMods + return mod.AllDataContainers .SelectMany(m => m.Files.Concat(m.FileSwaps)) .Where(kv => kv.Value.FullName.Equals(path, StringComparison.OrdinalIgnoreCase)) .Select(kv => kv.Key) @@ -103,7 +103,7 @@ public partial class ModEditWindow return []; // Filter then prepend the current option to ensure it's chosen first. - return mod.AllSubMods + return mod.AllDataContainers .Where(subMod => subMod != option) .Prepend(option) .SelectMany(subMod => subMod.Manipulations) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 4ecacece..70854fe7 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -187,8 +187,8 @@ public partial class ModEditWindow if (editor == null) return new QuickImportAction(owner._editor, FallbackOptionName, gamePath); - var subMod = editor.Option; - var optionName = subMod!.FullName; + var subMod = editor.Option!; + var optionName = subMod is IModOption o ? o.FullName : FallbackOptionName; if (gamePath.IsEmpty || file == null || editor.FileEditor.Changes) return new QuickImportAction(editor, optionName, gamePath); @@ -199,7 +199,7 @@ public partial class ModEditWindow if (mod == null) return new QuickImportAction(editor, optionName, gamePath); - var (preferredPath, subDirs) = GetPreferredPath(mod, subMod, owner._config.ReplaceNonAsciiOnImport); + var (preferredPath, subDirs) = GetPreferredPath(mod, subMod as IModOption, owner._config.ReplaceNonAsciiOnImport); var targetPath = new FullPath(Path.Combine(preferredPath.FullName, gamePath.ToString())).FullName; if (File.Exists(targetPath)) return new QuickImportAction(editor, optionName, gamePath); @@ -222,16 +222,16 @@ public partial class ModEditWindow { fileRegistry, }, _subDirs); - _editor.FileEditor.Apply(_editor.Mod!, (SubMod)_editor.Option!); + _editor.FileEditor.Apply(_editor.Mod!, _editor.Option!); return fileRegistry; } - private static (DirectoryInfo, int) GetPreferredPath(Mod mod, SubMod subMod, bool replaceNonAscii) + private static (DirectoryInfo, int) GetPreferredPath(Mod mod, IModOption? subMod, bool replaceNonAscii) { var path = mod.ModPath; var subDirs = 0; - if (subMod == mod.Default) + if (subMod == null) return (path, subDirs); var name = subMod.Name; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 3f5f6c37..21d14f78 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -77,10 +77,10 @@ public partial class ModEditWindow : Window, IDisposable _forceTextureStartPath = true; } - public void ChangeOption(SubMod? subMod) + public void ChangeOption(IModDataContainer? subMod) { - var (groupIdx, optionIdx) = subMod?.GetIndices() ?? (-1, 0); - _editor.LoadOption(groupIdx, optionIdx); + var (groupIdx, dataIdx) = subMod?.GetDataIndices() ?? (-1, 0); + _editor.LoadOption(groupIdx, dataIdx); } public void UpdateModels() @@ -111,7 +111,7 @@ public partial class ModEditWindow : Window, IDisposable }); var manipulations = 0; var subMods = 0; - var swaps = Mod!.AllSubMods.Sum(m => + var swaps = Mod!.AllDataContainers.Sum(m => { ++subMods; manipulations += m.Manipulations.Count; @@ -330,7 +330,7 @@ public partial class ModEditWindow : Window, IDisposable else if (ImGuiUtil.DrawDisabledButton("Re-Duplicate and Normalize Mod", Vector2.Zero, tt, !_allowReduplicate && !modifier)) { _editor.ModNormalizer.Normalize(Mod!); - _editor.ModNormalizer.Worker.ContinueWith(_ => _editor.LoadMod(Mod!, _editor.GroupIdx, _editor.OptionIdx), TaskScheduler.Default); + _editor.ModNormalizer.Worker.ContinueWith(_ => _editor.LoadMod(Mod!, _editor.GroupIdx, _editor.DataIdx), TaskScheduler.Default); } if (!_editor.Duplicates.Worker.IsCompleted) @@ -405,7 +405,7 @@ public partial class ModEditWindow : Window, IDisposable var width = new Vector2(ImGui.GetContentRegionAvail().X / 3, 0); var ret = false; if (ImGuiUtil.DrawDisabledButton(defaultOption, width, "Switch to the default option for the mod.\nThis resets unsaved changes.", - _editor.Option!.IsDefault)) + _editor.Option is DefaultSubMod)) { _editor.LoadOption(-1, 0); ret = true; @@ -414,7 +414,7 @@ public partial class ModEditWindow : Window, IDisposable ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton("Refresh Data", width, "Refresh data for the current option.\nThis resets unsaved changes.", false)) { - _editor.LoadMod(_editor.Mod!, _editor.GroupIdx, _editor.OptionIdx); + _editor.LoadMod(_editor.Mod!, _editor.GroupIdx, _editor.DataIdx); ret = true; } @@ -422,17 +422,17 @@ public partial class ModEditWindow : Window, IDisposable ImGui.SetNextItemWidth(width.X); style.Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); using var color = ImRaii.PushColor(ImGuiCol.Border, ColorId.FolderLine.Value()); - using var combo = ImRaii.Combo("##optionSelector", _editor.Option.FullName); + using var combo = ImRaii.Combo("##optionSelector", _editor.Option!.GetFullName()); if (!combo) return ret; - foreach (var (option, idx) in Mod!.AllSubMods.WithIndex()) + foreach (var (option, idx) in Mod!.AllDataContainers.WithIndex()) { using var id = ImRaii.PushId(idx); - if (ImGui.Selectable(option.FullName, option == _editor.Option)) + if (ImGui.Selectable(option.GetFullName(), option == _editor.Option)) { - var (groupIdx, optionIdx) = option.GetIndices(); - _editor.LoadOption(groupIdx, optionIdx); + var (groupIdx, dataIdx) = option.GetDataIndices(); + _editor.LoadOption(groupIdx, dataIdx); ret = true; } } @@ -455,7 +455,7 @@ public partial class ModEditWindow : Window, IDisposable var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option."; ImGui.NewLine(); if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, setsEqual)) - _editor.SwapEditor.Apply(_editor.Mod!, _editor.GroupIdx, _editor.OptionIdx); + _editor.SwapEditor.Apply(_editor.Mod!, _editor.GroupIdx, _editor.DataIdx); ImGui.SameLine(); tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; @@ -569,7 +569,7 @@ public partial class ModEditWindow : Window, IDisposable } if (Mod != null) - foreach (var option in Mod.AllSubMods) + foreach (var option in Mod.AllDataContainers) { foreach (var path in option.Files.Keys) { diff --git a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs index c34c7ef0..bed31ab8 100644 --- a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs @@ -50,8 +50,7 @@ public class ModMergeTab(ModMerger modMerger) ImGui.SameLine(); DrawCombo(size - ImGui.GetItemRectSize().X - ImGui.GetStyle().ItemSpacing.X); - var width = ImGui.GetItemRectSize(); - using (var g = ImRaii.Group()) + using (ImRaii.Group()) { using var disabled = ImRaii.Disabled(modMerger.MergeFromMod.HasOptions); var buttonWidth = (size - ImGui.GetStyle().ItemSpacing.X) / 2; @@ -124,13 +123,13 @@ public class ModMergeTab(ModMerger modMerger) ImGui.Dummy(Vector2.One); var buttonSize = new Vector2((size - 2 * ImGui.GetStyle().ItemSpacing.X) / 3, 0); if (ImGui.Button("Select All", buttonSize)) - modMerger.SelectedOptions.UnionWith(modMerger.MergeFromMod!.AllSubMods); + modMerger.SelectedOptions.UnionWith(modMerger.MergeFromMod!.AllDataContainers); ImGui.SameLine(); if (ImGui.Button("Unselect All", buttonSize)) modMerger.SelectedOptions.Clear(); ImGui.SameLine(); if (ImGui.Button("Invert Selection", buttonSize)) - modMerger.SelectedOptions.SymmetricExceptWith(modMerger.MergeFromMod!.AllSubMods); + modMerger.SelectedOptions.SymmetricExceptWith(modMerger.MergeFromMod!.AllDataContainers); DrawOptionTable(size); } @@ -144,7 +143,7 @@ public class ModMergeTab(ModMerger modMerger) private void DrawOptionTable(float size) { - var options = modMerger.MergeFromMod!.AllSubMods.ToList(); + var options = modMerger.MergeFromMod!.AllDataContainers.ToList(); var height = modMerger.Warnings.Count == 0 && modMerger.Error == null ? ImGui.GetContentRegionAvail().Y - 3 * ImGui.GetFrameHeightWithSpacing() : 8 * ImGui.GetFrameHeightWithSpacing(); @@ -176,47 +175,41 @@ public class ModMergeTab(ModMerger modMerger) if (ImGui.Checkbox("##check", ref selected)) Handle(option, selected); - if (option.IsDefault) + if (option.Group is not { } group) { - ImGuiUtil.DrawTableColumn(option.FullName); + ImGuiUtil.DrawTableColumn(option.GetFullName()); ImGui.TableNextColumn(); } else { - ImGuiUtil.DrawTableColumn(option.Name); - var group = option.Group; - var optionEnumerator = group switch - { - SingleModGroup single => single.OptionData, - MultiModGroup multi => multi.PrioritizedOptions.Select(o => o.Mod), - _ => [], - }; + ImGuiUtil.DrawTableColumn(option.GetName()); + ImGui.TableNextColumn(); ImGui.Selectable(group.Name, false); if (ImGui.BeginPopupContextItem("##groupContext")) { if (ImGui.MenuItem("Select All")) // ReSharper disable once PossibleMultipleEnumeration - foreach (var opt in optionEnumerator) + foreach (var opt in group.DataContainers) Handle(opt, true); if (ImGui.MenuItem("Unselect All")) // ReSharper disable once PossibleMultipleEnumeration - foreach (var opt in optionEnumerator) + foreach (var opt in group.DataContainers) Handle(opt, false); ImGui.EndPopup(); } } ImGui.TableNextColumn(); - ImGuiUtil.RightAlign(option.FileData.Count.ToString(), 3 * ImGuiHelpers.GlobalScale); + ImGuiUtil.RightAlign(option.Files.Count.ToString(), 3 * ImGuiHelpers.GlobalScale); ImGui.TableNextColumn(); - ImGuiUtil.RightAlign(option.FileSwapData.Count.ToString(), 3 * ImGuiHelpers.GlobalScale); + ImGuiUtil.RightAlign(option.FileSwaps.Count.ToString(), 3 * ImGuiHelpers.GlobalScale); ImGui.TableNextColumn(); ImGuiUtil.RightAlign(option.Manipulations.Count.ToString(), 3 * ImGuiHelpers.GlobalScale); continue; - void Handle(SubMod option2, bool selected2) + void Handle(IModDataContainer option2, bool selected2) { if (selected2) modMerger.SelectedOptions.Add(option2); diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 0dc694d8..c05f1ac1 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -486,7 +486,7 @@ public class ModPanelEditTab( EditOption(panel, single, groupIdx, optionIdx); break; case MultiModGroup multi: - for (var optionIdx = 0; optionIdx < multi.PrioritizedOptions.Count; ++optionIdx) + for (var optionIdx = 0; optionIdx < multi.OptionData.Count; ++optionIdx) EditOption(panel, multi, groupIdx, optionIdx); break; } @@ -542,7 +542,7 @@ public class ModPanelEditTab( if (group is not MultiModGroup multi) return; - if (Input.Priority("##Priority", groupIdx, optionIdx, multi.PrioritizedOptions[optionIdx].Priority, out var priority, + if (Input.Priority("##Priority", groupIdx, optionIdx, multi.OptionData[optionIdx].Priority, out var priority, 50 * UiHelpers.Scale)) panel._modManager.OptionEditor.ChangeOptionPriority(panel._mod, groupIdx, optionIdx, priority); @@ -557,7 +557,7 @@ public class ModPanelEditTab( var count = group switch { SingleModGroup single => single.OptionData.Count, - MultiModGroup multi => multi.PrioritizedOptions.Count, + MultiModGroup multi => multi.OptionData.Count, _ => throw new Exception($"Dragging options to an option group of type {group.GetType()} is not supported."), }; ImGui.TableNextColumn(); @@ -591,6 +591,9 @@ public class ModPanelEditTab( // Handle drag and drop to move options inside a group or into another group. private static void Source(IModGroup group, int groupIdx, int optionIdx) { + if (group is not ITexToolsGroup) + return; + using var source = ImRaii.DragDropSource(); if (!source) return; @@ -606,6 +609,9 @@ public class ModPanelEditTab( private static void Target(ModPanelEditTab panel, IModGroup group, int groupIdx, int optionIdx) { + if (group is not ITexToolsGroup) + return; + using var target = ImRaii.DragDropTarget(); if (!target.Success || !ImGuiUtil.IsDropping(DragDropLabel)) return; @@ -624,22 +630,12 @@ public class ModPanelEditTab( var sourceGroupIdx = _dragDropGroupIdx; var sourceOption = _dragDropOptionIdx; var sourceGroup = panel._mod.Groups[sourceGroupIdx]; - var currentCount = 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."), - }; - 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."), - }; + var currentCount = group.DataContainers.Count; + var option = ((ITexToolsGroup) sourceGroup).OptionData[_dragDropOptionIdx]; panel._delayedActions.Enqueue(() => { panel._modManager.OptionEditor.DeleteOption(panel._mod, sourceGroupIdx, sourceOption); - panel._modManager.OptionEditor.AddOption(panel._mod, groupIdx, option, priority); + panel._modManager.OptionEditor.AddOption(panel._mod, groupIdx, option); panel._modManager.OptionEditor.MoveOption(panel._mod, groupIdx, currentCount, optionIdx); }); } diff --git a/Penumbra/UI/ModsTab/ModPanelTabBar.cs b/Penumbra/UI/ModsTab/ModPanelTabBar.cs index 02ec9a32..1aaa7741 100644 --- a/Penumbra/UI/ModsTab/ModPanelTabBar.cs +++ b/Penumbra/UI/ModsTab/ModPanelTabBar.cs @@ -114,7 +114,7 @@ public class ModPanelTabBar if (ImGui.TabItemButton("Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip)) { _modEditWindow.ChangeMod(mod); - _modEditWindow.ChangeOption((SubMod)mod.Default); + _modEditWindow.ChangeOption(mod.Default); _modEditWindow.IsOpen = true; }