diff --git a/Penumbra.Api b/Penumbra.Api index 590629df..69d106b4 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit 590629df33f9ad92baddd1d65ec8c986f18d608a +Subproject commit 69d106b457eb0f73d4b4caf1234da5631fd6fbf0 diff --git a/Penumbra/Api/Api/ModSettingsApi.cs b/Penumbra/Api/Api/ModSettingsApi.cs index 039fbfa9..bfd134bb 100644 --- a/Penumbra/Api/Api/ModSettingsApi.cs +++ b/Penumbra/Api/Api/ModSettingsApi.cs @@ -11,6 +11,7 @@ using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; using Penumbra.Services; namespace Penumbra.Api.Api; @@ -254,7 +255,7 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting _1, int _2, bool inherited) => ModSettingChanged?.Invoke(type, collection.Id, mod?.ModPath.Name ?? string.Empty, inherited); - private void OnModOptionEdited(ModOptionChangeType type, Mod mod, int groupIndex, int optionIndex, int moveIndex) + private void OnModOptionEdited(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, int moveIndex) { switch (type) { diff --git a/Penumbra/Collections/Cache/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs index c1296414..9c104cef 100644 --- a/Penumbra/Collections/Cache/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -7,8 +7,10 @@ using Penumbra.Communication; using Penumbra.Interop.ResourceLoading; using Penumbra.Meta; using Penumbra.Mods; +using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.String.Classes; @@ -257,7 +259,7 @@ public class CollectionCacheManager : IDisposable } /// Prepare Changes by removing mods from caches with collections or add or reload mods. - private void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx) + private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, int movedToIdx) { if (type is ModOptionChangeType.PrepareChange) { diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index 1fe5b227..bfae2dc0 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -4,8 +4,10 @@ using OtterGui.Classes; using Penumbra.Communication; using Penumbra.Mods; using Penumbra.Mods.Editor; +using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; using Penumbra.Services; namespace Penumbra.Collections.Manager; @@ -290,7 +292,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable } /// Save all collections where the mod has settings and the change requires saving. - private void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx) + private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, int movedToIdx) { type.HandlingInfo(out var requiresSaving, out _, out _); if (!requiresSaving) @@ -298,7 +300,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable foreach (var collection in this) { - if (collection.Settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false) + if (collection.Settings[mod.Index]?.HandleChanges(type, mod, group, option, movedToIdx) ?? false) _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); } } diff --git a/Penumbra/Communication/ModOptionChanged.cs b/Penumbra/Communication/ModOptionChanged.cs index 0df58b5f..a20592ec 100644 --- a/Penumbra/Communication/ModOptionChanged.cs +++ b/Penumbra/Communication/ModOptionChanged.cs @@ -1,8 +1,10 @@ using OtterGui.Classes; -using Penumbra.Api; using Penumbra.Api.Api; using Penumbra.Mods; +using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; +using Penumbra.Mods.SubMods; +using static Penumbra.Communication.ModOptionChanged; namespace Penumbra.Communication; @@ -11,22 +13,23 @@ namespace Penumbra.Communication; /// /// Parameter is the type option change. /// Parameter is the changed mod. -/// Parameter is the index of the changed group inside the mod. -/// Parameter is the index of the changed option inside the group or -1 if it does not concern a specific option. -/// Parameter is the index of the group an option was moved to. +/// Parameter is the changed group inside the mod. +/// Parameter is the changed option inside the group or null if it does not concern a specific option. +/// Parameter is the changed data container inside the group or null if it does not concern a specific data container. +/// Parameter is the index of the group or option moved or deleted from. /// public sealed class ModOptionChanged() - : EventWrapper(nameof(ModOptionChanged)) + : EventWrapper(nameof(ModOptionChanged)) { public enum Priority { - /// + /// Api = int.MinValue, /// CollectionCacheManager = -100, - /// + /// ModCacheManager = 0, /// diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index eb6d0b0c..2b45ecbe 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -205,7 +205,7 @@ public partial class TexToolsImporter { var option = group.OptionList[idx]; _currentOptionName = option.Name; - options.Insert(idx, MultiSubMod.CreateForSaving(option.Name, option.Description, ModPriority.Default)); + options.Insert(idx, MultiSubMod.WithoutGroup(option.Name, option.Description, ModPriority.Default)); if (option.IsChecked) defaultSettings = Setting.Single(idx); } diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index 31aacbe1..84a832a2 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -60,7 +60,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co private void HandleDuplicate(Mod mod, FullPath duplicate, FullPath remaining, bool useModManager) { - ModEditor.ApplyToAllOptions(mod, HandleSubMod); + ModEditor.ApplyToAllContainers(mod, HandleSubMod); try { @@ -73,7 +73,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co return; - void HandleSubMod(IModDataContainer subMod, int groupIdx, int optionIdx) + void HandleSubMod(IModDataContainer subMod) { var changes = false; var dict = subMod.Files.ToDictionary(kvp => kvp.Key, @@ -82,14 +82,9 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co return; if (useModManager) - { - modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, dict, SaveType.ImmediateSync); - } + modManager.OptionEditor.SetFiles(subMod, dict, SaveType.ImmediateSync); else - { - subMod.Files = dict; - saveService.ImmediateSaveSync(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - } + saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, subMod, config.ReplaceNonAsciiOnImport)); } } diff --git a/Penumbra/Mods/Editor/ModEditor.cs b/Penumbra/Mods/Editor/ModEditor.cs index e1c5962f..37524da1 100644 --- a/Penumbra/Mods/Editor/ModEditor.cs +++ b/Penumbra/Mods/Editor/ModEditor.cs @@ -29,8 +29,8 @@ public class ModEditor( public int GroupIdx { get; private set; } public int DataIdx { get; private set; } - public IModGroup? Group { get; private set; } - public IModDataContainer? 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); @@ -63,10 +63,10 @@ public class ModEditor( { if (groupIdx == -1 && dataIdx == 0) { - Group = null; - Option = Mod.Default; - GroupIdx = groupIdx; - DataIdx = dataIdx; + Group = null; + Option = Mod.Default; + GroupIdx = groupIdx; + DataIdx = dataIdx; return; } @@ -75,18 +75,18 @@ public class ModEditor( Group = Mod.Groups[groupIdx]; if (dataIdx >= 0 && dataIdx < Group.DataContainers.Count) { - Option = Group.DataContainers[dataIdx]; - GroupIdx = groupIdx; - DataIdx = dataIdx; + Option = Group.DataContainers[dataIdx]; + GroupIdx = groupIdx; + DataIdx = dataIdx; return; } } } - Group = null; - Option = Mod?.Default; - GroupIdx = -1; - DataIdx = 0; + Group = null; + Option = Mod?.Default; + GroupIdx = -1; + DataIdx = 0; if (message) Penumbra.Log.Error($"Loading invalid option {groupIdx} {dataIdx} for Mod {Mod?.Name ?? "Unknown"}."); } @@ -105,23 +105,11 @@ 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 ApplyToAllContainers(Mod mod, Action action) { - action(mod.Default, -1, 0); - foreach (var (group, groupIdx) in mod.Groups.WithIndex()) - { - switch (group) - { - case SingleModGroup single: - for (var optionIdx = 0; optionIdx < single.OptionData.Count; ++optionIdx) - action(single.OptionData[optionIdx], groupIdx, optionIdx); - break; - case MultiModGroup multi: - for (var optionIdx = 0; optionIdx < multi.OptionData.Count; ++optionIdx) - action(multi.OptionData[optionIdx], groupIdx, optionIdx); - break; - } - } + action(mod.Default); + foreach (var container in mod.Groups.SelectMany(g => g.DataContainers)) + action(container); } // Does not delete the base directory itself even if it is completely empty at the end. diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index 00685c94..e2c0b726 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -24,8 +24,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu num += dict.TryAdd(path.Item2, file.File) ? 0 : 1; } - var (groupIdx, dataIdx) = option.GetDataIndices(); - modManager.OptionEditor.OptionSetFiles(mod, groupIdx, dataIdx, dict); + modManager.OptionEditor.SetFiles(option, dict); files.UpdatePaths(mod, option); Changes = false; return num; @@ -40,15 +39,15 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu /// Remove all path redirections where the pointed-to file does not exist. public void RemoveMissingPaths(Mod mod, IModDataContainer option) { - void HandleSubMod(IModDataContainer subMod, int groupIdx, int optionIdx) + void HandleSubMod(IModDataContainer subMod) { var newDict = subMod.Files.Where(kvp => CheckAgainstMissing(mod, subMod, kvp.Value, kvp.Key, subMod == option)) .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); if (newDict.Count != subMod.Files.Count) - modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, newDict); + modManager.OptionEditor.SetFiles(subMod, newDict); } - ModEditor.ApplyToAllOptions(mod, HandleSubMod); + ModEditor.ApplyToAllContainers(mod, HandleSubMod); files.ClearMissingFiles(); } diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs index 0f629bc7..3a6f4a81 100644 --- a/Penumbra/Mods/Editor/ModMerger.cs +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -16,7 +16,7 @@ public class ModMerger : IDisposable { private readonly Configuration _config; private readonly CommunicatorService _communicator; - private readonly ModOptionEditor _editor; + private readonly ModGroupEditor _editor; private readonly ModFileSystemSelector _selector; private readonly DuplicateManager _duplicates; private readonly ModManager _mods; @@ -32,14 +32,14 @@ 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 IReadOnlyList Warnings = []; public Exception? Error { get; private set; } - public ModMerger(ModManager mods, ModOptionEditor editor, ModFileSystemSelector selector, DuplicateManager duplicates, + public ModMerger(ModManager mods, ModGroupEditor editor, ModFileSystemSelector selector, DuplicateManager duplicates, CommunicatorService communicator, ModCreator creator, Configuration config) { _editor = editor; @@ -100,22 +100,23 @@ public class ModMerger : IDisposable var (group, groupIdx, groupCreated) = _editor.FindOrAddModGroup(MergeToMod!, originalGroup.Type, originalGroup.Name); if (groupCreated) _createdGroups.Add(groupIdx); - if (group.Type != originalGroup.Type) - ((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}."); + if (group == null) + throw new Exception( + $"The merged group {originalGroup.Name} already existed, but had a different type than the original group of type {originalGroup.Type}."); foreach (var originalOption in group.DataContainers) { - var (option, _, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, originalOption.GetName()); + var (option, _, optionCreated) = _editor.FindOrAddOption(group, originalOption.GetName()); if (optionCreated) { - _createdOptions.Add((IModDataOption)option); - MergeIntoOption([originalOption], (IModDataOption)option, false); + _createdOptions.Add(option!); + // #TODO DataContainer <> Option. + MergeIntoOption([originalOption], (IModDataContainer)option!, false); } else { throw new Exception( - $"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}: The option {option.FullName} already existed."); + $"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}: The option {option!.FullName} already existed."); } } } @@ -138,9 +139,9 @@ public class ModMerger : IDisposable var (group, groupIdx, groupCreated) = _editor.FindOrAddModGroup(MergeToMod!, GroupType.Multi, groupName, SaveType.None); if (groupCreated) _createdGroups.Add(groupIdx); - var (option, _, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, optionName, SaveType.None); + var (option, _, optionCreated) = _editor.FindOrAddOption(group!, optionName, SaveType.None); if (optionCreated) - _createdOptions.Add((IModDataOption)option); + _createdOptions.Add(option!); var dir = ModCreator.NewOptionDirectory(MergeToMod!.ModPath, groupName, _config.ReplaceNonAsciiOnImport); if (!dir.Exists) _createdDirectories.Add(dir.FullName); @@ -148,7 +149,8 @@ public class ModMerger : IDisposable if (!dir.Exists) _createdDirectories.Add(dir.FullName); CopyFiles(dir); - MergeIntoOption(MergeFromMod!.AllDataContainers.Reverse(), (IModDataOption)option, true); + // #TODO DataContainer <> Option. + MergeIntoOption(MergeFromMod!.AllDataContainers.Reverse(), (IModDataContainer)option!, true); } private void MergeIntoOption(IEnumerable mergeOptions, IModDataContainer option, bool fromFileToFile) @@ -184,10 +186,9 @@ public class ModMerger : IDisposable } } - 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); + _editor.SetFiles(option, redirections, SaveType.None); + _editor.SetFileSwaps(option, swaps, SaveType.None); + _editor.SetManipulations(option, manips, SaveType.ImmediateSync); return; bool GetFullPath(FullPath input, out FullPath ret) @@ -261,30 +262,31 @@ public class ModMerger : IDisposable if (mods.Count == 1) { var files = CopySubModFiles(mods[0], dir); - _editor.OptionSetFiles(result, -1, 0, files); - _editor.OptionSetFileSwaps(result, -1, 0, mods[0].FileSwaps); - _editor.OptionSetManipulations(result, -1, 0, mods[0].Manipulations); + _editor.SetFiles(result.Default, files); + _editor.SetFileSwaps(result.Default, mods[0].FileSwaps); + _editor.SetManipulations(result.Default, mods[0].Manipulations); } else { foreach (var originalOption in mods) { - if (originalOption.Group is not {} originalGroup) + 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].FileSwaps); - _editor.OptionSetManipulations(result, -1, 0, mods[0].Manipulations); + _editor.SetFiles(result.Default, files); + _editor.SetFileSwaps(result.Default, mods[0].FileSwaps); + _editor.SetManipulations(result.Default, mods[0].Manipulations); } else { - 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); + // TODO DataContainer <> Option. + var (group, _, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name); + var (option, _, _) = _editor.FindOrAddOption(group!, 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.FileSwaps); - _editor.OptionSetManipulations(result, groupIdx, optionIdx, originalOption.Manipulations); + _editor.SetFiles((IModDataContainer)option, files); + _editor.SetFileSwaps((IModDataContainer)option, originalOption.FileSwaps); + _editor.SetManipulations((IModDataContainer)option, originalOption.Manipulations); } } } @@ -339,16 +341,15 @@ public class ModMerger : IDisposable { foreach (var option in _createdOptions) { - var (groupIdx, optionIdx) = option.GetOptionIndices(); - _editor.DeleteOption(MergeToMod!, groupIdx, optionIdx); + _editor.DeleteOption(option); Penumbra.Log.Verbose($"[Merger] Removed option {option.FullName}."); } foreach (var group in _createdGroups) { var groupName = MergeToMod!.Groups[group]; - _editor.DeleteModGroup(MergeToMod!, group); - Penumbra.Log.Verbose($"[Merger] Removed option group {groupName}."); + _editor.DeleteModGroup(groupName); + Penumbra.Log.Verbose($"[Merger] Removed option group {groupName.Name}."); } foreach (var dir in _createdDirectories) diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index dee700d5..2f7fd04c 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -145,12 +145,12 @@ public class ModMetaEditor(ModManager modManager) Split(currentOption.Manipulations); } - public void Apply(Mod mod, int groupIdx, int optionIdx) + public void Apply(IModDataContainer container) { if (!Changes) return; - modManager.OptionEditor.OptionSetManipulations(mod, groupIdx, optionIdx, Recombine().ToHashSet()); + modManager.OptionEditor.SetManipulations(container, Recombine().ToHashSet()); Changes = false; } diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index e2088b32..437600c9 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -283,12 +283,12 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) switch (group) { case SingleModGroup single: - foreach (var (_, optionIdx) in single.OptionData.WithIndex()) - _modManager.OptionEditor.OptionSetFiles(Mod, groupIdx, optionIdx, _redirections[groupIdx + 1][optionIdx]); + foreach (var (option, optionIdx) in single.OptionData.WithIndex()) + _modManager.OptionEditor.SetFiles(option, _redirections[groupIdx + 1][optionIdx]); break; case MultiModGroup multi: - foreach (var (_, optionIdx) in multi.OptionData.WithIndex()) - _modManager.OptionEditor.OptionSetFiles(Mod, groupIdx, optionIdx, _redirections[groupIdx + 1][optionIdx]); + foreach (var (option, optionIdx) in multi.OptionData.WithIndex()) + _modManager.OptionEditor.SetFiles(option, _redirections[groupIdx + 1][optionIdx]); break; } } diff --git a/Penumbra/Mods/Editor/ModSwapEditor.cs b/Penumbra/Mods/Editor/ModSwapEditor.cs index 3247cfdf..0250efae 100644 --- a/Penumbra/Mods/Editor/ModSwapEditor.cs +++ b/Penumbra/Mods/Editor/ModSwapEditor.cs @@ -17,12 +17,12 @@ public class ModSwapEditor(ModManager modManager) Changes = false; } - public void Apply(Mod mod, int groupIdx, int optionIdx) + public void Apply(IModDataContainer container) { if (!Changes) return; - modManager.OptionEditor.OptionSetFileSwaps(mod, groupIdx, optionIdx, _swaps); + modManager.OptionEditor.SetFileSwaps(container, _swaps); Changes = false; } diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs index b13799cd..a268ba0f 100644 --- a/Penumbra/Mods/Groups/IModGroup.cs +++ b/Penumbra/Mods/Groups/IModGroup.cs @@ -9,7 +9,7 @@ namespace Penumbra.Mods.Groups; public interface ITexToolsGroup { - public IReadOnlyList OptionData { get; } + public IReadOnlyList OptionData { get; } } public interface IModGroup @@ -17,22 +17,19 @@ public interface IModGroup public const int MaxMultiOptions = 63; public Mod Mod { get; } - public string Name { get; } + public string Name { get; set; } public string Description { get; set; } public GroupType Type { get; } public ModPriority Priority { get; set; } public Setting DefaultSettings { get; set; } - public FullPath? FindBestMatch(Utf8GamePath gamePath); - public int AddOption(Mod mod, string name, string description = ""); + public FullPath? FindBestMatch(Utf8GamePath gamePath); + public IModOption? AddOption(string name, string description = ""); 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); diff --git a/Penumbra/Mods/Groups/ImcModGroup.cs b/Penumbra/Mods/Groups/ImcModGroup.cs new file mode 100644 index 00000000..e233f82e --- /dev/null +++ b/Penumbra/Mods/Groups/ImcModGroup.cs @@ -0,0 +1,158 @@ +using Newtonsoft.Json; +using Penumbra.Api.Enums; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; +using Penumbra.String.Classes; + +namespace Penumbra.Mods.Groups; + +public class ImcModGroup(Mod mod) : IModGroup +{ + public const int DisabledIndex = 30; + public const int NumAttributes = 10; + + public Mod Mod { get; } = mod; + public string Name { get; set; } = "Option"; + public string Description { get; set; } = "A single IMC manipulation."; + + public GroupType Type + => GroupType.Imc; + + public ModPriority Priority { get; set; } = ModPriority.Default; + public Setting DefaultSettings { get; set; } = Setting.Zero; + + public PrimaryId PrimaryId; + public SecondaryId SecondaryId; + public ObjectType ObjectType; + public BodySlot BodySlot; + public EquipSlot EquipSlot; + public Variant Variant; + + public ImcEntry DefaultEntry; + + public FullPath? FindBestMatch(Utf8GamePath gamePath) + => null; + + private bool _canBeDisabled = false; + + public bool CanBeDisabled + { + get => _canBeDisabled; + set + { + _canBeDisabled = value; + if (!value) + DefaultSettings = FixSetting(DefaultSettings); + } + } + + public IModOption? AddOption(string name, string description = "") + { + uint fullMask = GetFullMask(); + var firstUnset = (byte)BitOperations.TrailingZeroCount(~fullMask); + // All attributes handled. + if (firstUnset >= NumAttributes) + return null; + + var groupIdx = Mod.Groups.IndexOf(this); + if (groupIdx < 0) + return null; + + var subMod = new ImcSubMod(this) + { + Name = name, + Description = description, + AttributeIndex = firstUnset, + }; + OptionData.Add(subMod); + return subMod; + } + + public readonly List OptionData = []; + + public IReadOnlyList Options + => OptionData; + + public IReadOnlyList DataContainers + => []; + + public bool IsOption + => CanBeDisabled || OptionData.Count > 0; + + public int GetIndex() + => ModGroup.GetIndex(this); + + private ushort GetCurrentMask(Setting setting) + { + var mask = DefaultEntry.AttributeMask; + for (var i = 0; i < OptionData.Count; ++i) + { + if (!setting.HasFlag(i)) + continue; + + var option = OptionData[i]; + mask |= option.Attribute; + } + + return mask; + } + + private ushort GetFullMask() + => GetCurrentMask(Setting.AllBits(63)); + + private ImcManipulation GetManip(ushort mask) + => new(ObjectType, BodySlot, PrimaryId, SecondaryId.Id, Variant.Id, EquipSlot, + DefaultEntry with { AttributeMask = mask }); + + + public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) + { + if (CanBeDisabled && setting.HasFlag(DisabledIndex)) + return; + + var mask = GetCurrentMask(setting); + var imc = GetManip(mask); + manipulations.Add(imc); + } + + public Setting FixSetting(Setting setting) + => new(setting.Value & (((1ul << OptionData.Count) - 1) | (CanBeDisabled ? 1ul << DisabledIndex : 0))); + + public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null) + { + ModSaveGroup.WriteJsonBase(jWriter, this); + jWriter.WritePropertyName(nameof(ObjectType)); + jWriter.WriteValue(ObjectType.ToString()); + jWriter.WritePropertyName(nameof(BodySlot)); + jWriter.WriteValue(BodySlot.ToString()); + jWriter.WritePropertyName(nameof(EquipSlot)); + jWriter.WriteValue(EquipSlot.ToString()); + jWriter.WritePropertyName(nameof(PrimaryId)); + jWriter.WriteValue(PrimaryId.Id); + jWriter.WritePropertyName(nameof(SecondaryId)); + jWriter.WriteValue(SecondaryId.Id); + jWriter.WritePropertyName(nameof(Variant)); + jWriter.WriteValue(Variant.Id); + jWriter.WritePropertyName(nameof(DefaultEntry)); + serializer.Serialize(jWriter, DefaultEntry); + jWriter.WritePropertyName("Options"); + jWriter.WriteStartArray(); + foreach (var option in OptionData) + { + jWriter.WriteStartObject(); + SubMod.WriteModOption(jWriter, option); + jWriter.WritePropertyName(nameof(option.AttributeIndex)); + jWriter.WriteValue(option.AttributeIndex); + jWriter.WriteEndObject(); + } + + jWriter.WriteEndArray(); + jWriter.WriteEndObject(); + } + + public (int Redirections, int Swaps, int Manips) GetCounts() + => (0, 0, 1); +} diff --git a/Penumbra/Mods/Groups/ModGroup.cs b/Penumbra/Mods/Groups/ModGroup.cs index da302714..8b55a035 100644 --- a/Penumbra/Mods/Groups/ModGroup.cs +++ b/Penumbra/Mods/Groups/ModGroup.cs @@ -5,6 +5,7 @@ namespace Penumbra.Mods.Groups; public static class ModGroup { + /// Create a new mod group based on the given type. public static IModGroup Create(Mod mod, GroupType type, string name) { var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1; @@ -20,6 +21,11 @@ public static class ModGroup Name = name, Priority = maxPriority, }, + GroupType.Imc => new ImcModGroup(mod) + { + Name = name, + Priority = maxPriority, + }, _ => throw new ArgumentOutOfRangeException(nameof(type), type, null), }; } @@ -38,5 +44,14 @@ public static class ModGroup } return (redirectionCount, swapCount, manipCount); + } + + public static int GetIndex(IModGroup group) + { + var groupIndex = group.Mod.Groups.IndexOf(group); + if (groupIndex < 0) + throw new Exception($"Mod {group.Mod.Name} from Group {group.Name} does not contain this group."); + + return groupIndex; } } diff --git a/Penumbra/Mods/Groups/ModSaveGroup.cs b/Penumbra/Mods/Groups/ModSaveGroup.cs index 332879cb..efdcde09 100644 --- a/Penumbra/Mods/Groups/ModSaveGroup.cs +++ b/Penumbra/Mods/Groups/ModSaveGroup.cs @@ -1,9 +1,9 @@ -using Newtonsoft.Json; -using Penumbra.Mods.SubMods; -using Penumbra.Services; - -namespace Penumbra.Mods.Groups; - +using Newtonsoft.Json; +using Penumbra.Mods.SubMods; +using Penumbra.Services; + +namespace Penumbra.Mods.Groups; + public readonly struct ModSaveGroup : ISavable { private readonly DirectoryInfo _basePath; @@ -12,25 +12,21 @@ public readonly struct ModSaveGroup : ISavable private readonly DefaultSubMod? _defaultMod; private readonly bool _onlyAscii; - public ModSaveGroup(Mod mod, int groupIdx, bool onlyAscii) - { - _basePath = mod.ModPath; - _groupIdx = groupIdx; - if (_groupIdx < 0) - _defaultMod = mod.Default; - else - _group = mod.Groups[_groupIdx]; - _onlyAscii = onlyAscii; - } - - public ModSaveGroup(DirectoryInfo basePath, IModGroup group, int groupIdx, bool onlyAscii) + private ModSaveGroup(DirectoryInfo basePath, IModGroup group, int groupIndex, bool onlyAscii) { _basePath = basePath; _group = group; - _groupIdx = groupIdx; + _groupIdx = groupIndex; _onlyAscii = onlyAscii; } + public static ModSaveGroup WithoutMod(DirectoryInfo basePath, IModGroup group, int groupIndex, bool onlyAscii) + => new(basePath, group, groupIndex, onlyAscii); + + public ModSaveGroup(IModGroup group, bool onlyAscii) + : this(group.Mod.ModPath, group, group.GetIndex(), onlyAscii) + { } + public ModSaveGroup(DirectoryInfo basePath, DefaultSubMod @default, bool onlyAscii) { _basePath = basePath; @@ -39,6 +35,33 @@ public readonly struct ModSaveGroup : ISavable _onlyAscii = onlyAscii; } + public ModSaveGroup(DirectoryInfo basePath, IModDataContainer container, bool onlyAscii) + { + _basePath = basePath; + _defaultMod = container as DefaultSubMod; + _onlyAscii = onlyAscii; + if (_defaultMod == null) + { + _groupIdx = -1; + _group = null; + } + else + { + _group = container.Group!; + _groupIdx = _group.GetIndex(); + } + } + + public ModSaveGroup(IModDataContainer container, bool onlyAscii) + { + _basePath = (container.Mod as Mod)?.ModPath + ?? throw new Exception("Invalid save group from default data container without base path."); // Should not happen. + _defaultMod = null; + _onlyAscii = onlyAscii; + _group = container.Group!; + _groupIdx = _group.GetIndex(); + } + public string ToFilename(FilenameService fileNames) => fileNames.OptionGroupFile(_basePath.FullName, _groupIdx, _group?.Name ?? string.Empty, _onlyAscii); @@ -59,7 +82,7 @@ public readonly struct ModSaveGroup : ISavable { jWriter.WriteStartObject(); jWriter.WritePropertyName(nameof(group.Name)); - jWriter.WriteValue(group!.Name); + jWriter.WriteValue(group.Name); jWriter.WritePropertyName(nameof(group.Description)); jWriter.WriteValue(group.Description); jWriter.WritePropertyName(nameof(group.Priority)); @@ -69,4 +92,4 @@ public readonly struct ModSaveGroup : ISavable jWriter.WritePropertyName(nameof(group.DefaultSettings)); jWriter.WriteValue(group.DefaultSettings.Value); } -} +} diff --git a/Penumbra/Mods/Groups/MultiModGroup.cs b/Penumbra/Mods/Groups/MultiModGroup.cs index 6b352f66..a0034be0 100644 --- a/Penumbra/Mods/Groups/MultiModGroup.cs +++ b/Penumbra/Mods/Groups/MultiModGroup.cs @@ -3,7 +3,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; using OtterGui.Classes; -using OtterGui.Filesystem; using Penumbra.Api.Enums; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Settings; @@ -18,11 +17,11 @@ 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; } = 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 @@ -39,28 +38,28 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup .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 = "") + public IModOption? AddOption(string name, string description = "") { - var groupIdx = mod.Groups.IndexOf(this); + var groupIdx = Mod.Groups.IndexOf(this); if (groupIdx < 0) - return -1; + return null; - var subMod = new MultiSubMod(mod, this) + var subMod = new MultiSubMod(this) { - Name = name, + Name = name, Description = description, }; OptionData.Add(subMod); - return OptionData.Count - 1; + return subMod; } public static MultiModGroup? Load(Mod mod, JObject json) { var ret = new MultiModGroup(mod) { - Name = json[nameof(Name)]?.ToObject() ?? string.Empty, - Description = json[nameof(Description)]?.ToObject() ?? string.Empty, - Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, + Name = json[nameof(Name)]?.ToObject() ?? string.Empty, + Description = json[nameof(Description)]?.ToObject() ?? string.Empty, + Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, }; if (ret.Name.Length == 0) @@ -78,7 +77,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup break; } - var subMod = new MultiSubMod(mod, ret, child); + var subMod = new MultiSubMod(ret, child); ret.OptionData.Add(subMod); } @@ -87,42 +86,21 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup return ret; } - public IModGroup Convert(GroupType type) + public SingleModGroup ConvertToSingle() { - switch (type) + var single = new SingleModGroup(Mod) { - case GroupType.Multi: return this; - case GroupType.Single: - var single = new SingleModGroup(Mod) - { - Name = Name, - Description = Description, - Priority = Priority, - DefaultSettings = DefaultSettings.TurnMulti(OptionData.Count), - }; - 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 (!OptionData.Move(optionIdxFrom, optionIdxTo)) - return false; - - DefaultSettings = DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo); - return true; + Name = Name, + Description = Description, + Priority = Priority, + DefaultSettings = DefaultSettings.TurnMulti(OptionData.Count), + }; + single.OptionData.AddRange(OptionData.Select(o => o.ConvertToSingle(single))); + return single; } 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; - } + => ModGroup.GetIndex(this); public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) { @@ -156,15 +134,15 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup => ModGroup.GetCountsBase(this); public Setting FixSetting(Setting setting) - => new(setting.Value & (1ul << OptionData.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) + internal static MultiModGroup WithoutMod(string name) => new(null!) { Name = name, }; - IReadOnlyList ITexToolsGroup.OptionData + IReadOnlyList ITexToolsGroup.OptionData => OptionData; } diff --git a/Penumbra/Mods/Groups/SingleModGroup.cs b/Penumbra/Mods/Groups/SingleModGroup.cs index ac85e2bc..0776c2af 100644 --- a/Penumbra/Mods/Groups/SingleModGroup.cs +++ b/Penumbra/Mods/Groups/SingleModGroup.cs @@ -1,7 +1,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui; -using OtterGui.Filesystem; using Penumbra.Api.Enums; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Settings; @@ -16,31 +15,28 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup public GroupType Type => GroupType.Single; - public Mod Mod { get; set; } = mod; - public string Name { get; set; } = "Option"; - public string Description { get; set; } = "A mutually exclusive group of settings."; - public ModPriority Priority { get; set; } - public Setting DefaultSettings { get; set; } + public Mod Mod { get; } = mod; + public string Name { get; set; } = "Option"; + public string Description { get; set; } = "A mutually exclusive group of settings."; + public ModPriority Priority { get; set; } + public Setting DefaultSettings { get; set; } public readonly List OptionData = []; - IReadOnlyList ITexToolsGroup.OptionData - => OptionData; - public FullPath? FindBestMatch(Utf8GamePath gamePath) => OptionData .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 = "") + public IModOption AddOption(string name, string description = "") { - var subMod = new SingleSubMod(mod, this) + var subMod = new SingleSubMod(this) { - Name = name, + Name = name, Description = description, }; OptionData.Add(subMod); - return OptionData.Count - 1; + return subMod; } public IReadOnlyList Options @@ -57,9 +53,9 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup var options = json["Options"]; var ret = new SingleModGroup(mod) { - Name = json[nameof(Name)]?.ToObject() ?? string.Empty, - Description = json[nameof(Description)]?.ToObject() ?? string.Empty, - Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, + Name = json[nameof(Name)]?.ToObject() ?? string.Empty, + Description = json[nameof(Description)]?.ToObject() ?? string.Empty, + Priority = json[nameof(Priority)]?.ToObject() ?? ModPriority.Default, DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? Setting.Zero, }; if (ret.Name.Length == 0) @@ -68,7 +64,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup if (options != null) foreach (var child in options.Children()) { - var subMod = new SingleSubMod(mod, ret, child); + var subMod = new SingleSubMod(ret, child); ret.OptionData.Add(subMod); } @@ -76,57 +72,21 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup return ret; } - public IModGroup Convert(GroupType type) + public MultiModGroup ConvertToMulti() { - switch (type) + var multi = new MultiModGroup(Mod) { - case GroupType.Single: return this; - case GroupType.Multi: - var multi = new MultiModGroup(Mod) - { - Name = Name, - Description = Description, - Priority = Priority, - DefaultSettings = Setting.Multi((int)DefaultSettings.Value), - }; - multi.OptionData.AddRange(OptionData.Select((o, i) => o.ConvertToMulti(Mod, multi, new ModPriority(i)))); - return multi; - default: throw new ArgumentOutOfRangeException(nameof(type), type, null); - } + Name = Name, + Description = Description, + Priority = Priority, + DefaultSettings = Setting.Multi((int)DefaultSettings.Value), + }; + multi.OptionData.AddRange(OptionData.Select((o, i) => o.ConvertToMulti(multi, new ModPriority(i)))); + return multi; } - public bool MoveOption(int optionIdxFrom, int optionIdxTo) - { - if (!OptionData.Move(optionIdxFrom, optionIdxTo)) - return false; - - var currentIndex = DefaultSettings.AsIndex; - // Update default settings with the move. - if (currentIndex == optionIdxFrom) - { - DefaultSettings = Setting.Single(optionIdxTo); - } - else if (optionIdxFrom < optionIdxTo) - { - if (currentIndex > optionIdxFrom && currentIndex <= optionIdxTo) - DefaultSettings = Setting.Single(currentIndex - 1); - } - else if (currentIndex < optionIdxFrom && currentIndex >= optionIdxTo) - { - DefaultSettings = Setting.Single(currentIndex + 1); - } - - 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 int GetIndex() + => ModGroup.GetIndex(this); public void AddData(Setting setting, Dictionary redirections, HashSet manipulations) => OptionData[setting.AsIndex].AddDataTo(redirections, manipulations); @@ -160,4 +120,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup { Name = name, }; + + IReadOnlyList ITexToolsGroup.OptionData + => OptionData; } diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index 1545811e..449405a0 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -1,3 +1,4 @@ +using OtterGui.Classes; using Penumbra.Collections; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; @@ -8,6 +9,7 @@ using Penumbra.Meta; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; namespace Penumbra.Mods.ItemSwap; @@ -40,8 +42,7 @@ public class ItemSwapContainer NoSwaps, } - public bool WriteMod(ModManager manager, Mod mod, WriteType writeType = WriteType.NoSwaps, DirectoryInfo? directory = null, - int groupIndex = -1, int optionIndex = 0) + public bool WriteMod(ModManager manager, Mod mod, IModDataContainer container, WriteType writeType = WriteType.NoSwaps, DirectoryInfo? directory = null) { var convertedManips = new HashSet(Swaps.Count); var convertedFiles = new Dictionary(Swaps.Count); @@ -80,9 +81,9 @@ public class ItemSwapContainer } } - manager.OptionEditor.OptionSetFiles(mod, groupIndex, optionIndex, convertedFiles); - manager.OptionEditor.OptionSetFileSwaps(mod, groupIndex, optionIndex, convertedSwaps); - manager.OptionEditor.OptionSetManipulations(mod, groupIndex, optionIndex, convertedManips); + manager.OptionEditor.SetFiles(container, convertedFiles, SaveType.None); + manager.OptionEditor.SetFileSwaps(container, convertedSwaps, SaveType.None); + manager.OptionEditor.SetManipulations(container, convertedManips, SaveType.ImmediateSync); return true; } catch (Exception e) diff --git a/Penumbra/Mods/Manager/ImcModGroupEditor.cs b/Penumbra/Mods/Manager/ImcModGroupEditor.cs new file mode 100644 index 00000000..4e2b2194 --- /dev/null +++ b/Penumbra/Mods/Manager/ImcModGroupEditor.cs @@ -0,0 +1,38 @@ +using OtterGui.Classes; +using OtterGui.Filesystem; +using OtterGui.Services; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; +using Penumbra.Services; + +namespace Penumbra.Mods.Manager; + +public sealed class ImcModGroupEditor(CommunicatorService communicator, SaveService saveService, Configuration config) + : ModOptionEditor(communicator, saveService, config), IService +{ + protected override ImcModGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync) + => new(mod) + { + Name = newName, + Priority = priority, + }; + + protected override ImcSubMod? CloneOption(ImcModGroup group, IModOption option) + => null; + + protected override void RemoveOption(ImcModGroup group, int optionIndex) + { + group.OptionData.RemoveAt(optionIndex); + group.DefaultSettings = group.FixSetting(group.DefaultSettings); + } + + protected override bool MoveOption(ImcModGroup group, int optionIdxFrom, int optionIdxTo) + { + if (!group.OptionData.Move(optionIdxFrom, optionIdxTo)) + return false; + + group.DefaultSettings = group.DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo); + return true; + } +} diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index 3ff1a333..0669696f 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -2,6 +2,8 @@ using Penumbra.Communication; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Groups; +using Penumbra.Mods.SubMods; using Penumbra.Services; namespace Penumbra.Mods.Manager; @@ -103,7 +105,7 @@ public class ModCacheManager : IDisposable } } - private void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int _, int _2) + private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, int fromIdx) { switch (type) { diff --git a/Penumbra/Mods/Manager/ModGroupEditor.cs b/Penumbra/Mods/Manager/ModGroupEditor.cs new file mode 100644 index 00000000..9f41fa6f --- /dev/null +++ b/Penumbra/Mods/Manager/ModGroupEditor.cs @@ -0,0 +1,289 @@ +using System.Text.RegularExpressions; +using Dalamud.Interface.Internal.Notifications; +using OtterGui.Classes; +using OtterGui.Filesystem; +using OtterGui.Services; +using Penumbra.Api.Enums; +using Penumbra.GameData.Data; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; +using Penumbra.Services; +using Penumbra.String.Classes; +using Penumbra.Util; +using static FFXIVClientStructs.FFXIV.Client.UI.Misc.ConfigModule; + +namespace Penumbra.Mods.Manager; + +public enum ModOptionChangeType +{ + GroupRenamed, + GroupAdded, + GroupDeleted, + GroupMoved, + GroupTypeChanged, + PriorityChanged, + OptionAdded, + OptionDeleted, + OptionMoved, + OptionFilesChanged, + OptionFilesAdded, + OptionSwapsChanged, + OptionMetaChanged, + DisplayChange, + PrepareChange, + DefaultOptionChanged, +} + +public class ModGroupEditor( + SingleModGroupEditor singleEditor, + MultiModGroupEditor multiEditor, + ImcModGroupEditor imcEditor, + CommunicatorService Communicator, + SaveService SaveService, + Configuration Config) : IService +{ + public SingleModGroupEditor SingleEditor + => singleEditor; + + public MultiModGroupEditor MultiEditor + => multiEditor; + + public ImcModGroupEditor ImcEditor + => imcEditor; + + /// Change the settings stored as default options in a mod. + public void ChangeModGroupDefaultOption(IModGroup group, Setting defaultOption) + { + if (group.DefaultSettings == defaultOption) + return; + + group.DefaultSettings = defaultOption; + SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, group.Mod, group, null, null, -1); + } + + /// Rename an option group if possible. + public void RenameModGroup(IModGroup group, string newName) + { + var oldName = group.Name; + if (oldName == newName || !VerifyFileName(group.Mod, group, newName, true)) + return; + + SaveService.ImmediateDelete(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + group.Name = newName; + SaveService.ImmediateSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, group.Mod, group, null, null, -1); + } + + /// Delete a given option group. Fires an event to prepare before actually deleting. + public void DeleteModGroup(IModGroup group) + { + var mod = group.Mod; + var idx = group.GetIndex(); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, group, null, null, -1); + mod.Groups.RemoveAt(idx); + SaveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, null, null, null, idx); + } + + /// Move the index of a given option group. + public void MoveModGroup(IModGroup group, int groupIdxTo) + { + var mod = group.Mod; + var idxFrom = group.GetIndex(); + if (!mod.Groups.Move(idxFrom, groupIdxTo)) + return; + + SaveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, group, null, null, idxFrom); + } + + /// Change the internal priority of the given option group. + public void ChangeGroupPriority(IModGroup group, ModPriority newPriority) + { + if (group.Priority == newPriority) + return; + + group.Priority = newPriority; + SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, group.Mod, group, null, null, -1); + } + + /// Change the description of the given option group. + public void ChangeGroupDescription(IModGroup group, string newDescription) + { + if (group.Description == newDescription) + return; + + group.Description = newDescription; + SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, group.Mod, group, null, null, -1); + } + + /// Rename the given option. + public void RenameOption(IModOption option, string newName) + { + if (option.Name == newName) + return; + + option.Name = newName; + SaveService.QueueSave(new ModSaveGroup(option.Group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, option.Mod, option.Group, option, null, -1); + } + + /// Change the description of the given option. + public void ChangeOptionDescription(IModOption option, string newDescription) + { + if (option.Description == newDescription) + return; + + option.Description = newDescription; + SaveService.QueueSave(new ModSaveGroup(option.Group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, option.Mod, option.Group, option, null, -1); + } + + /// Set the meta manipulations for a given option. Replaces existing manipulations. + public void SetManipulations(IModDataContainer subMod, HashSet manipulations, SaveType saveType = SaveType.Queue) + { + 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)subMod.Mod, subMod.Group, null, subMod, -1); + subMod.Manipulations.SetTo(manipulations); + SaveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + } + + /// Set the file redirections for a given option. Replaces existing redirections. + public void SetFiles(IModDataContainer subMod, IReadOnlyDictionary replacements, SaveType saveType = SaveType.Queue) + { + if (subMod.Files.SetEquals(replacements)) + return; + + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + subMod.Files.SetTo(replacements); + SaveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + } + + /// Add additional file redirections to a given option, keeping already existing ones. Only fires an event if anything is actually added. + public void AddFiles(IModDataContainer subMod, IReadOnlyDictionary additions) + { + var oldCount = subMod.Files.Count; + subMod.Files.AddFrom(additions); + if (oldCount != subMod.Files.Count) + { + SaveService.QueueSave(new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + } + } + + /// Set the file swaps for a given option. Replaces existing swaps. + public void SetFileSwaps(IModDataContainer subMod, IReadOnlyDictionary swaps, SaveType saveType = SaveType.Queue) + { + if (subMod.FileSwaps.SetEquals(swaps)) + return; + + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + subMod.FileSwaps.SetTo(swaps); + SaveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1); + } + + /// Verify that a new option group name is unique in this mod. + public static bool VerifyFileName(Mod mod, IModGroup? group, string newName, bool message) + { + var path = newName.RemoveInvalidPathSymbols(); + if (path.Length != 0 + && !mod.Groups.Any(o => !ReferenceEquals(o, group) + && string.Equals(o.Name.RemoveInvalidPathSymbols(), path, StringComparison.OrdinalIgnoreCase))) + return true; + + if (message) + Penumbra.Messager.NotificationMessage( + $"Could not name option {newName} because option with same filename {path} already exists.", + NotificationType.Warning, false); + + return false; + } + + public void DeleteOption(IModOption option) + { + switch (option) + { + case SingleSubMod s: + SingleEditor.DeleteOption(s); + return; + case MultiSubMod m: + MultiEditor.DeleteOption(m); + return; + case ImcSubMod i: + ImcEditor.DeleteOption(i); + return; + } + } + + public IModOption? AddOption(IModGroup group, IModOption option) + => group switch + { + SingleModGroup s => SingleEditor.AddOption(s, option), + MultiModGroup m => MultiEditor.AddOption(m, option), + ImcModGroup i => ImcEditor.AddOption(i, option), + _ => null, + }; + + public IModOption? AddOption(IModGroup group, string newName) + => group switch + { + SingleModGroup s => SingleEditor.AddOption(s, newName), + MultiModGroup m => MultiEditor.AddOption(m, newName), + ImcModGroup i => ImcEditor.AddOption(i, newName), + _ => null, + }; + + public IModGroup? AddModGroup(Mod mod, GroupType type, string newName, SaveType saveType = SaveType.ImmediateSync) + => type switch + { + GroupType.Single => SingleEditor.AddModGroup(mod, newName, saveType), + GroupType.Multi => MultiEditor.AddModGroup(mod, newName, saveType), + GroupType.Imc => ImcEditor.AddModGroup(mod, newName, saveType), + _ => null, + }; + + public (IModGroup?, int, bool) FindOrAddModGroup(Mod mod, GroupType type, string name, SaveType saveType = SaveType.ImmediateSync) + => type switch + { + GroupType.Single => SingleEditor.FindOrAddModGroup(mod, name, saveType), + GroupType.Multi => MultiEditor.FindOrAddModGroup(mod, name, saveType), + GroupType.Imc => ImcEditor.FindOrAddModGroup(mod, name, saveType), + _ => (null, -1, false), + }; + + public (IModOption?, int, bool) FindOrAddOption(IModGroup group, string name, SaveType saveType = SaveType.ImmediateSync) + => group switch + { + SingleModGroup s => SingleEditor.FindOrAddOption(s, name, saveType), + MultiModGroup m => MultiEditor.FindOrAddOption(m, name, saveType), + ImcModGroup i => ImcEditor.FindOrAddOption(i, name, saveType), + _ => (null, -1, false), + }; + + public void MoveOption(IModOption option, int toIdx) + { + switch (option) + { + case SingleSubMod s: + SingleEditor.MoveOption(s, toIdx); + return; + case MultiSubMod m: + MultiEditor.MoveOption(m, toIdx); + return; + case ImcSubMod i: + ImcEditor.MoveOption(i, toIdx); + return; + } + } +} diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index d912e292..7a266a31 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -1,4 +1,3 @@ -using System.Security.AccessControl; using Penumbra.Communication; using Penumbra.Mods.Editor; using Penumbra.Services; @@ -32,14 +31,14 @@ public sealed class ModManager : ModStorage, IDisposable private readonly Configuration _config; private readonly CommunicatorService _communicator; - public readonly ModCreator Creator; - public readonly ModDataEditor DataEditor; - public readonly ModOptionEditor OptionEditor; + public readonly ModCreator Creator; + public readonly ModDataEditor DataEditor; + public readonly ModGroupEditor OptionEditor; public DirectoryInfo BasePath { get; private set; } = null!; public bool Valid { get; private set; } - public ModManager(Configuration config, CommunicatorService communicator, ModDataEditor dataEditor, ModOptionEditor optionEditor, + public ModManager(Configuration config, CommunicatorService communicator, ModDataEditor dataEditor, ModGroupEditor optionEditor, ModCreator creator) { _config = config; diff --git a/Penumbra/Mods/Manager/ModMigration.cs b/Penumbra/Mods/Manager/ModMigration.cs index f160d5bd..c7eb7cc5 100644 --- a/Penumbra/Mods/Manager/ModMigration.cs +++ b/Penumbra/Mods/Manager/ModMigration.cs @@ -83,8 +83,8 @@ public static partial class ModMigration mod.Default.FileSwaps.Add(gamePath, swapPath); creator.IncorporateMetaChanges(mod.Default, mod.ModPath, true); - foreach (var (_, index) in mod.Groups.WithIndex()) - saveService.ImmediateSave(new ModSaveGroup(mod, index, creator.Config.ReplaceNonAsciiOnImport)); + foreach (var group in mod.Groups) + saveService.ImmediateSave(new ModSaveGroup(group, creator.Config.ReplaceNonAsciiOnImport)); // Delete meta files. foreach (var file in seenMetaFiles.Where(f => f.Exists)) @@ -112,7 +112,7 @@ public static partial class ModMigration } fileVersion = 1; - saveService.ImmediateSave(new ModSaveGroup(mod, -1, creator.Config.ReplaceNonAsciiOnImport)); + saveService.ImmediateSave(new ModSaveGroup(mod.ModPath, mod.Default, creator.Config.ReplaceNonAsciiOnImport)); return true; } @@ -176,7 +176,7 @@ public static partial class ModMigration private static SingleSubMod SubModFromOption(ModCreator creator, Mod mod, SingleModGroup group, OptionV0 option, HashSet seenMetaFiles) { - var subMod = new SingleSubMod(mod, group) + var subMod = new SingleSubMod(group) { Name = option.OptionName, Description = option.OptionDesc, @@ -189,7 +189,7 @@ public static partial class ModMigration private static MultiSubMod SubModFromOption(ModCreator creator, Mod mod, MultiModGroup group, OptionV0 option, ModPriority priority, HashSet seenMetaFiles) { - var subMod = new MultiSubMod(mod, group) + var subMod = new MultiSubMod(group) { Name = option.OptionName, Description = option.OptionDesc, @@ -219,7 +219,7 @@ public static partial class ModMigration [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] public GroupType SelectionType = GroupType.Single; - public List Options = new(); + public List Options = []; public OptionGroupV0() { } @@ -236,12 +236,12 @@ public static partial class ModMigration var token = JToken.Load(reader); if (token.Type == JTokenType.Array) - return token.ToObject>() ?? new HashSet(); + return token.ToObject>() ?? []; var tmp = token.ToObject(); return tmp != null ? new HashSet { tmp } - : new HashSet(); + : []; } public override bool CanWrite diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index c6122ea8..7370a933 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -1,384 +1,122 @@ -using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Classes; -using OtterGui.Filesystem; -using Penumbra.Api.Enums; -using Penumbra.Meta.Manipulations; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; -using Penumbra.String.Classes; -using Penumbra.Util; namespace Penumbra.Mods.Manager; -public enum ModOptionChangeType +public abstract class ModOptionEditor( + CommunicatorService communicator, + SaveService saveService, + Configuration config) + where TGroup : class, IModGroup + where TOption : class, IModOption { - GroupRenamed, - GroupAdded, - GroupDeleted, - GroupMoved, - GroupTypeChanged, - PriorityChanged, - OptionAdded, - OptionDeleted, - OptionMoved, - OptionFilesChanged, - OptionFilesAdded, - OptionSwapsChanged, - OptionMetaChanged, - DisplayChange, - PrepareChange, - DefaultOptionChanged, -} - -public class ModOptionEditor(CommunicatorService communicator, SaveService saveService, Configuration config) -{ - /// Change the type of a group given by mod and index to type, if possible. - public void ChangeModGroupType(Mod mod, int groupIdx, GroupType type) - { - var group = mod.Groups[groupIdx]; - if (group.Type == type) - return; - - mod.Groups[groupIdx] = group.Convert(type); - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1); - } - - /// Change the settings stored as default options in a mod. - public void ChangeModGroupDefaultOption(Mod mod, int groupIdx, Setting defaultOption) - { - var group = mod.Groups[groupIdx]; - if (group.DefaultSettings == defaultOption) - return; - - group.DefaultSettings = defaultOption; - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, mod, groupIdx, -1, -1); - } - - /// Rename an option group if possible. - public void RenameModGroup(Mod mod, int groupIdx, string newName) - { - var group = mod.Groups[groupIdx]; - var oldName = group.Name; - if (oldName == newName || !VerifyFileName(mod, group, newName, true)) - return; - - saveService.ImmediateDelete(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - _ = group switch - { - SingleModGroup s => s.Name = newName, - MultiModGroup m => m.Name = newName, - _ => newName, - }; - - saveService.ImmediateSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1); - } + protected readonly CommunicatorService Communicator = communicator; + protected readonly SaveService SaveService = saveService; + protected readonly Configuration Config = config; /// Add a new, empty option group of the given type and name. - public void AddModGroup(Mod mod, GroupType type, string newName, SaveType saveType = SaveType.ImmediateSync) + public TGroup? AddModGroup(Mod mod, string newName, SaveType saveType = SaveType.ImmediateSync) { - if (!VerifyFileName(mod, null, newName, true)) - return; + if (!ModGroupEditor.VerifyFileName(mod, null, newName, true)) + return null; - var idx = mod.Groups.Count; - var group = ModGroup.Create(mod, type, newName); + var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1; + var group = CreateGroup(mod, newName, maxPriority); mod.Groups.Add(group); - saveService.Save(saveType, new ModSaveGroup(mod, idx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, idx, -1, -1); + SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, group, null, null, -1); + return group; } /// Add a new mod, empty option group of the given type and name if it does not exist already. - public (IModGroup, int, bool) FindOrAddModGroup(Mod mod, GroupType type, string newName, SaveType saveType = SaveType.ImmediateSync) + public (TGroup, int, bool) FindOrAddModGroup(Mod mod, string newName, SaveType saveType = SaveType.ImmediateSync) { var idx = mod.Groups.IndexOf(g => g.Name == newName); if (idx >= 0) - return (mod.Groups[idx], idx, false); + { + var existingGroup = mod.Groups[idx] as TGroup + ?? throw new Exception($"Mod group with name {newName} exists, but is of the wrong type."); + return (existingGroup, idx, false); + } - AddModGroup(mod, type, newName, saveType); - if (mod.Groups[^1].Name != newName) + idx = mod.Groups.Count; + if (AddModGroup(mod, newName, saveType) is not { } group) throw new Exception($"Could not create new mod group with name {newName}."); - return (mod.Groups[^1], mod.Groups.Count - 1, true); - } - - /// Delete a given option group. Fires an event to prepare before actually deleting. - public void DeleteModGroup(Mod mod, int groupIdx) - { - communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1); - mod.Groups.RemoveAt(groupIdx); - saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1); - } - - /// Move the index of a given option group. - public void MoveModGroup(Mod mod, int groupIdxFrom, int groupIdxTo) - { - if (!mod.Groups.Move(groupIdxFrom, groupIdxTo)) - return; - - saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo); - } - - /// Change the description of the given option group. - public void ChangeGroupDescription(Mod mod, int groupIdx, string newDescription) - { - var group = mod.Groups[groupIdx]; - if (group.Description == newDescription) - return; - - group.Description = newDescription; - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, -1, -1); - } - - /// Change the description of the given option. - public void ChangeOptionDescription(Mod mod, int groupIdx, int optionIdx, string newDescription) - { - var option = mod.Groups[groupIdx].Options[optionIdx]; - if (option.Description == newDescription) - return; - - option.Description = newDescription; - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); - } - - /// Change the internal priority of the given option group. - public void ChangeGroupPriority(Mod mod, int groupIdx, ModPriority newPriority) - { - var group = mod.Groups[groupIdx]; - if (group.Priority == newPriority) - return; - - group.Priority = newPriority; - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1); - } - - /// Change the internal priority of the given option. - public void ChangeOptionPriority(Mod mod, int groupIdx, int optionIdx, ModPriority newPriority) - { - switch (mod.Groups[groupIdx]) - { - case MultiModGroup multi: - if (multi.OptionData[optionIdx].Priority == newPriority) - return; - - multi.OptionData[optionIdx].Priority = newPriority; - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1); - return; - } - } - - /// Rename the given option. - public void RenameOption(Mod mod, int groupIdx, int optionIdx, string newName) - { - var option = mod.Groups[groupIdx].Options[optionIdx]; - if (option.Name == newName) - return; - - option.Name = newName; - - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); + return (group, idx, true); } /// Add a new empty option of the given name for the given group. - public int AddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue) + public TOption? AddOption(TGroup group, string newName, SaveType saveType = SaveType.Queue) { - var group = mod.Groups[groupIdx]; - var idx = group.AddOption(mod, newName); - if (idx < 0) - return -1; + if (group.AddOption(newName) is not TOption option) + return null; - saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1); - return idx; + SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, group.Mod, group, option, null, -1); + return option; } /// Add a new empty option of the given name for the given group if it does not exist already. - public (IModOption, int, bool) FindOrAddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue) + public (TOption, int, bool) FindOrAddOption(TGroup group, string newName, SaveType saveType = SaveType.Queue) { - var group = mod.Groups[groupIdx]; - var idx = group.Options.IndexOf(o => o.Name == newName); + var idx = group.Options.IndexOf(o => o.Name == newName); if (idx >= 0) - return (group.Options[idx], idx, false); + { + var existingOption = group.Options[idx] as TOption + ?? throw new Exception($"Mod option with name {newName} exists, but is of the wrong type."); // Should never happen. + return (existingOption, idx, false); + } - idx = group.AddOption(mod, newName); - if (idx < 0) + if (AddOption(group, newName, saveType) is not { } option) 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 (group.Options[idx], idx, true); + return (option, idx, true); } /// Add an existing option to a given group. - public void AddOption(Mod mod, int groupIdx, IModOption option) + public TOption? AddOption(TGroup group, IModOption option) { - var group = mod.Groups[groupIdx]; - int idx; - switch (group) - { - 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; - var newOption = new SingleSubMod(s.Mod, s) - { - Name = option.Name, - Description = option.Description, - }; - if (option is IModDataContainer data) - SubMod.Clone(data, newOption); - s.OptionData.Add(newOption); - break; - } - case MultiModGroup m: - { - 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) - SubMod.Clone(data, newOption); - m.OptionData.Add(newOption); - break; - } - default: return; - } + if (CloneOption(group, option) is not { } clonedOption) + return null; - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1); + SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, group.Mod, group, clonedOption, null, -1); + return clonedOption; } /// Delete the given option from the given group. - public void DeleteOption(Mod mod, int groupIdx, int optionIdx) + public void DeleteOption(TOption option) { - var group = mod.Groups[groupIdx]; - communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); - switch (group) - { - case SingleModGroup s: - s.OptionData.RemoveAt(optionIdx); - - break; - case MultiModGroup m: - m.OptionData.RemoveAt(optionIdx); - break; - } - - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1); + var mod = option.Mod; + var group = option.Group; + var optionIdx = option.GetIndex(); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, group, option, null, -1); + RemoveOption((TGroup)group, optionIdx); + SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, group, null, null, optionIdx); } /// Move an option inside the given option group. - public void MoveOption(Mod mod, int groupIdx, int optionIdxFrom, int optionIdxTo) + public void MoveOption(TOption option, int optionIdxTo) { - var group = mod.Groups[groupIdx]; - if (!group.MoveOption(optionIdxFrom, optionIdxTo)) + var idx = option.GetIndex(); + var group = (TGroup)option.Group; + if (!MoveOption(group, idx, optionIdxTo)) return; - saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, mod, groupIdx, optionIdxFrom, optionIdxTo); + SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, group.Mod, group, option, null, idx); } - /// Set the meta manipulations for a given option. Replaces existing manipulations. - public void OptionSetManipulations(Mod mod, int groupIdx, int dataContainerIdx, HashSet manipulations, - SaveType saveType = SaveType.Queue) - { - 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, dataContainerIdx, -1); - subMod.Manipulations.SetTo(manipulations); - saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); - 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 dataContainerIdx, IReadOnlyDictionary replacements, - SaveType saveType = SaveType.Queue) - { - var subMod = GetSubMod(mod, groupIdx, dataContainerIdx); - if (subMod.Files.SetEquals(replacements)) - return; - - 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, 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 dataContainerIdx, IReadOnlyDictionary additions) - { - 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, dataContainerIdx, -1); - } - } - - /// Set the file swaps for a given option. Replaces existing swaps. - public void OptionSetFileSwaps(Mod mod, int groupIdx, int dataContainerIdx, IReadOnlyDictionary swaps, - SaveType saveType = SaveType.Queue) - { - var subMod = GetSubMod(mod, groupIdx, dataContainerIdx); - if (subMod.FileSwaps.SetEquals(swaps)) - return; - - 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, dataContainerIdx, -1); - } - - - /// Verify that a new option group name is unique in this mod. - public static bool VerifyFileName(Mod mod, IModGroup? group, string newName, bool message) - { - var path = newName.RemoveInvalidPathSymbols(); - if (path.Length != 0 - && !mod.Groups.Any(o => !ReferenceEquals(o, group) - && string.Equals(o.Name.RemoveInvalidPathSymbols(), path, StringComparison.OrdinalIgnoreCase))) - return true; - - if (message) - Penumbra.Messager.NotificationMessage( - $"Could not name option {newName} because option with same filename {path} already exists.", - NotificationType.Warning, false); - - return false; - } - - /// Get the correct option for the given group and option index. - private static IModDataContainer GetSubMod(Mod mod, int groupIdx, int dataContainerIdx) - { - if (groupIdx == -1 && dataContainerIdx == 0) - return mod.Default; - - return mod.Groups[groupIdx].DataContainers[dataContainerIdx]; - } + protected abstract TGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync); + protected abstract TOption? CloneOption(TGroup group, IModOption option); + protected abstract void RemoveOption(TGroup group, int optionIndex); + protected abstract bool MoveOption(TGroup group, int optionIdxFrom, int optionIdxTo); } public static class ModOptionChangeTypeExtension diff --git a/Penumbra/Mods/Manager/MultiModGroupEditor.cs b/Penumbra/Mods/Manager/MultiModGroupEditor.cs new file mode 100644 index 00000000..e6b2bac1 --- /dev/null +++ b/Penumbra/Mods/Manager/MultiModGroupEditor.cs @@ -0,0 +1,84 @@ +using OtterGui.Classes; +using OtterGui.Filesystem; +using OtterGui.Services; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; +using Penumbra.Services; + +namespace Penumbra.Mods.Manager; + +public sealed class MultiModGroupEditor(CommunicatorService communicator, SaveService saveService, Configuration config) + : ModOptionEditor(communicator, saveService, config), IService +{ + public void ChangeToSingle(MultiModGroup group) + { + var idx = group.GetIndex(); + var singleGroup = group.ConvertToSingle(); + group.Mod.Groups[idx] = singleGroup; + SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, group.Mod, group, null, null, -1); + } + + /// Change the internal priority of the given option. + public void ChangeOptionPriority(MultiSubMod option, ModPriority newPriority) + { + if (option.Priority == newPriority) + return; + + option.Priority = newPriority; + SaveService.QueueSave(new ModSaveGroup(option.Group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, option.Mod, option.Group, option, null, -1); + } + + protected override MultiModGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync) + => new(mod) + { + Name = newName, + Priority = priority, + }; + + protected override MultiSubMod? CloneOption(MultiModGroup group, IModOption option) + { + if (group.OptionData.Count >= IModGroup.MaxMultiOptions) + { + Penumbra.Log.Error( + $"Could not add option {option.Name} to {group.Name} for mod {group.Mod.Name}, " + + $"since only up to {IModGroup.MaxMultiOptions} options are supported in one group."); + return null; + } + + var newOption = new MultiSubMod(group) + { + Name = option.Name, + Description = option.Description, + }; + + if (option is IModDataContainer data) + { + SubMod.Clone(data, newOption); + if (option is MultiSubMod m) + newOption.Priority = m.Priority; + else + newOption.Priority = new ModPriority(group.OptionData.Max(o => o.Priority.Value) + 1); + } + + group.OptionData.Add(newOption); + return newOption; + } + + protected override void RemoveOption(MultiModGroup group, int optionIndex) + { + group.OptionData.RemoveAt(optionIndex); + group.DefaultSettings = group.DefaultSettings.RemoveBit(optionIndex); + } + + protected override bool MoveOption(MultiModGroup group, int optionIdxFrom, int optionIdxTo) + { + if (!group.OptionData.Move(optionIdxFrom, optionIdxTo)) + return false; + + group.DefaultSettings = group.DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo); + return true; + } +} diff --git a/Penumbra/Mods/Manager/SingleModGroupEditor.cs b/Penumbra/Mods/Manager/SingleModGroupEditor.cs new file mode 100644 index 00000000..4999ff60 --- /dev/null +++ b/Penumbra/Mods/Manager/SingleModGroupEditor.cs @@ -0,0 +1,57 @@ +using OtterGui.Classes; +using OtterGui.Filesystem; +using OtterGui.Services; +using Penumbra.Mods.Groups; +using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; +using Penumbra.Services; + +namespace Penumbra.Mods.Manager; + +public sealed class SingleModGroupEditor(CommunicatorService communicator, SaveService saveService, Configuration config) + : ModOptionEditor(communicator, saveService, config), IService +{ + public void ChangeToMulti(SingleModGroup group) + { + var idx = group.GetIndex(); + var multiGroup = group.ConvertToMulti(); + group.Mod.Groups[idx] = multiGroup; + SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); + Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, group.Mod, multiGroup, null, null, -1); + } + + protected override SingleModGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync) + => new(mod) + { + Name = newName, + Priority = priority, + }; + + protected override SingleSubMod CloneOption(SingleModGroup group, IModOption option) + { + var newOption = new SingleSubMod(group) + { + Name = option.Name, + Description = option.Description, + }; + if (option is IModDataContainer data) + SubMod.Clone(data, newOption); + group.OptionData.Add(newOption); + return newOption; + } + + protected override void RemoveOption(SingleModGroup group, int optionIndex) + { + group.OptionData.RemoveAt(optionIndex); + group.DefaultSettings = group.DefaultSettings.RemoveSingle(optionIndex); + } + + protected override bool MoveOption(SingleModGroup group, int optionIdxFrom, int optionIdxTo) + { + if (!group.OptionData.Move(optionIdxFrom, optionIdxTo)) + return false; + + group.DefaultSettings = group.DefaultSettings.MoveSingle(optionIdxFrom, optionIdxTo); + return true; + } +} diff --git a/Penumbra/Mods/ModCreator.cs b/Penumbra/Mods/ModCreator.cs index 40f943c8..47261c6d 100644 --- a/Penumbra/Mods/ModCreator.cs +++ b/Penumbra/Mods/ModCreator.cs @@ -90,7 +90,7 @@ public partial class ModCreator( var changes = false; foreach (var file in _saveService.FileNames.GetOptionGroupFiles(mod)) { - var group = LoadModGroup(mod, file, mod.Groups.Count); + var group = LoadModGroup(mod, file); if (group != null && mod.Groups.All(g => g.Name != group.Name)) { changes = changes @@ -244,12 +244,12 @@ public partial class ModCreator( { case GroupType.Multi: { - var group = MultiModGroup.CreateForSaving(name); + var group = MultiModGroup.WithoutMod(name); group.Description = desc; group.Priority = priority; group.DefaultSettings = defaultSettings; - group.OptionData.AddRange(subMods.Select(s => s.Clone(null!, group))); - _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); + group.OptionData.AddRange(subMods.Select(s => s.Clone(group))); + _saveService.ImmediateSaveSync(ModSaveGroup.WithoutMod(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); break; } case GroupType.Single: @@ -258,8 +258,8 @@ public partial class ModCreator( group.Description = desc; group.Priority = priority; group.DefaultSettings = defaultSettings; - group.OptionData.AddRange(subMods.Select(s => s.ConvertToSingle(null!, group))); - _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); + group.OptionData.AddRange(subMods.Select(s => s.ConvertToSingle(group))); + _saveService.ImmediateSaveSync(ModSaveGroup.WithoutMod(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); break; } } @@ -272,7 +272,7 @@ public partial class ModCreator( .Select(f => (Utf8GamePath.FromFile(f, optionFolder, out var gamePath, true), gamePath, new FullPath(f))) .Where(t => t.Item1); - var mod = MultiSubMod.CreateForSaving(option.Name, option.Description, priority); + var mod = MultiSubMod.WithoutGroup(option.Name, option.Description, priority); foreach (var (_, gamePath, file) in list) mod.Files.TryAdd(gamePath, file); @@ -295,7 +295,7 @@ public partial class ModCreator( } IncorporateMetaChanges(mod.Default, directory, true); - _saveService.ImmediateSaveSync(new ModSaveGroup(mod, -1, Config.ReplaceNonAsciiOnImport)); + _saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, mod.Default, Config.ReplaceNonAsciiOnImport)); } /// Return the name of a new valid directory based on the base directory and the given name. @@ -422,7 +422,7 @@ public partial class ModCreator( /// Load an option group for a specific mod by its file and index. - private static IModGroup? LoadModGroup(Mod mod, FileInfo file, int groupIdx) + private static IModGroup? LoadModGroup(Mod mod, FileInfo file) { if (!File.Exists(file.FullName)) return null; @@ -442,7 +442,7 @@ public partial class ModCreator( } return null; - } + } internal static void DeleteDeleteList(IEnumerable deleteList, bool delete) { diff --git a/Penumbra/Mods/Settings/ModSettings.cs b/Penumbra/Mods/Settings/ModSettings.cs index db9e0521..39ee1860 100644 --- a/Penumbra/Mods/Settings/ModSettings.cs +++ b/Penumbra/Mods/Settings/ModSettings.cs @@ -4,6 +4,7 @@ using Penumbra.Api.Enums; using Penumbra.Mods.Editor; using Penumbra.Mods.Groups; using Penumbra.Mods.Manager; +using Penumbra.Mods.SubMods; namespace Penumbra.Mods.Settings; @@ -45,63 +46,64 @@ public class ModSettings } // Automatically react to changes in a mods available options. - public bool HandleChanges(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx) + public bool HandleChanges(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, int fromIdx) { switch (type) { case ModOptionChangeType.GroupRenamed: return true; case ModOptionChangeType.GroupAdded: // Add new empty setting for new mod. - Settings.Insert(groupIdx, mod.Groups[groupIdx].DefaultSettings); + Settings.Insert(group!.GetIndex(), group.DefaultSettings); return true; case ModOptionChangeType.GroupDeleted: // Remove setting for deleted mod. - Settings.RemoveAt(groupIdx); + Settings.RemoveAt(fromIdx); return true; case ModOptionChangeType.GroupTypeChanged: { // Fix settings for a changed group type. // Single -> Multi: set single as enabled, rest as disabled // Multi -> Single: set the first enabled option or 0. - var group = mod.Groups[groupIdx]; - var config = Settings[groupIdx]; - Settings[groupIdx] = group.Type switch + var idx = group!.GetIndex(); + var config = Settings[idx]; + Settings[idx] = group.Type switch { GroupType.Single => config.TurnMulti(group.Options.Count), GroupType.Multi => Setting.Multi((int)config.Value), _ => config, }; - return config != Settings[groupIdx]; + return config != Settings[idx]; } case ModOptionChangeType.OptionDeleted: { // Single -> select the previous option if any. // Multi -> excise the corresponding bit. - var group = mod.Groups[groupIdx]; - var config = Settings[groupIdx]; - Settings[groupIdx] = group.Type switch + var groupIdx = group!.GetIndex(); + var config = Settings[groupIdx]; + Settings[groupIdx] = group!.Type switch { - GroupType.Single => config.AsIndex >= optionIdx - ? config.AsIndex > 1 ? Setting.Single(config.AsIndex - 1) : Setting.Zero - : config, - GroupType.Multi => config.RemoveBit(optionIdx), - _ => config, + GroupType.Single => config.RemoveSingle(fromIdx), + GroupType.Multi => config.RemoveBit(fromIdx), + GroupType.Imc => config.RemoveBit(fromIdx), + _ => config, }; return config != Settings[groupIdx]; } case ModOptionChangeType.GroupMoved: // Move the group the same way. - return Settings.Move(groupIdx, movedToIdx); + return Settings.Move(fromIdx, group!.GetIndex()); case ModOptionChangeType.OptionMoved: { // Single -> select the moved option if it was currently selected // Multi -> move the corresponding bit - var group = mod.Groups[groupIdx]; - var config = Settings[groupIdx]; - Settings[groupIdx] = group.Type switch + var groupIdx = group!.GetIndex(); + var toIdx = option!.GetIndex(); + var config = Settings[groupIdx]; + Settings[groupIdx] = group!.Type switch { - GroupType.Single => config.AsIndex == optionIdx ? Setting.Single(movedToIdx) : config, - GroupType.Multi => config.MoveBit(optionIdx, movedToIdx), + GroupType.Single => config.MoveSingle(fromIdx, toIdx), + GroupType.Multi => config.MoveBit(fromIdx, toIdx), + GroupType.Imc => config.MoveBit(fromIdx, toIdx), _ => config, }; return config != Settings[groupIdx]; diff --git a/Penumbra/Mods/Settings/Setting.cs b/Penumbra/Mods/Settings/Setting.cs index 231529b8..059cbf51 100644 --- a/Penumbra/Mods/Settings/Setting.cs +++ b/Penumbra/Mods/Settings/Setting.cs @@ -41,6 +41,34 @@ public readonly record struct Setting(ulong Value) public Setting TurnMulti(int count) => new(Math.Max((ulong)Math.Min(count - 1, BitOperations.TrailingZeroCount(Value)), 0)); + public Setting RemoveSingle(int singleIdx) + { + var settingIndex = AsIndex; + if (settingIndex >= singleIdx) + return settingIndex > 1 ? Single(settingIndex - 1) : Zero; + + return this; + } + + public Setting MoveSingle(int singleIdxFrom, int singleIdxTo) + { + var currentIndex = AsIndex; + if (currentIndex == singleIdxFrom) + return Single(singleIdxTo); + + if (singleIdxFrom < singleIdxTo) + { + if (currentIndex > singleIdxFrom && currentIndex <= singleIdxTo) + return Single(currentIndex - 1); + } + else if (currentIndex < singleIdxFrom && currentIndex >= singleIdxTo) + { + return Single(currentIndex + 1); + } + + return this; + } + public ModPriority AsPriority => new((int)(Value & 0xFFFFFFFF)); diff --git a/Penumbra/Mods/SubMods/IModOption.cs b/Penumbra/Mods/SubMods/IModOption.cs index 83d632a0..ecfcf91a 100644 --- a/Penumbra/Mods/SubMods/IModOption.cs +++ b/Penumbra/Mods/SubMods/IModOption.cs @@ -1,10 +1,15 @@ +using Penumbra.Mods.Groups; + namespace Penumbra.Mods.SubMods; public interface IModOption { + public Mod Mod { get; } + public IModGroup Group { get; } + public string Name { get; set; } public string FullName { get; } public string Description { get; set; } - public (int GroupIndex, int OptionIndex) GetOptionIndices(); + public int GetIndex(); } diff --git a/Penumbra/Mods/SubMods/ImcSubMod.cs b/Penumbra/Mods/SubMods/ImcSubMod.cs new file mode 100644 index 00000000..167c8a6c --- /dev/null +++ b/Penumbra/Mods/SubMods/ImcSubMod.cs @@ -0,0 +1,32 @@ +using Penumbra.Mods.Groups; + +namespace Penumbra.Mods.SubMods; + +public class ImcSubMod(ImcModGroup group) : IModOption +{ + public readonly ImcModGroup Group = group; + + public Mod Mod + => Group.Mod; + + public byte AttributeIndex; + + public ushort Attribute + => (ushort)(1 << AttributeIndex); + + Mod IModOption.Mod + => Mod; + + IModGroup IModOption.Group + => Group; + + public string Name { get; set; } = "Part"; + + public string FullName + => $"{Group.Name}: {Name}"; + + public string Description { get; set; } = string.Empty; + + public int GetIndex() + => SubMod.GetIndex(this); +} diff --git a/Penumbra/Mods/SubMods/MultiSubMod.cs b/Penumbra/Mods/SubMods/MultiSubMod.cs index 3bcaffab..c01dcce9 100644 --- a/Penumbra/Mods/SubMods/MultiSubMod.cs +++ b/Penumbra/Mods/SubMods/MultiSubMod.cs @@ -4,21 +4,21 @@ using Penumbra.Mods.Settings; namespace Penumbra.Mods.SubMods; -public class MultiSubMod(Mod mod, MultiModGroup group) : OptionSubMod(mod, group) +public class MultiSubMod(MultiModGroup group) : OptionSubMod(group) { public ModPriority Priority { get; set; } = ModPriority.Default; - public MultiSubMod(Mod mod, MultiModGroup group, JToken json) - : this(mod, group) + public MultiSubMod(MultiModGroup group, JToken json) + : this(group) { SubMod.LoadOptionData(json, this); - SubMod.LoadDataContainer(json, this, mod.ModPath); + SubMod.LoadDataContainer(json, this, group.Mod.ModPath); Priority = json[nameof(IModGroup.Priority)]?.ToObject() ?? ModPriority.Default; } - public MultiSubMod Clone(Mod mod, MultiModGroup group) + public MultiSubMod Clone(MultiModGroup group) { - var ret = new MultiSubMod(mod, group) + var ret = new MultiSubMod(group) { Name = Name, Description = Description, @@ -29,9 +29,9 @@ public class MultiSubMod(Mod mod, MultiModGroup group) : OptionSubMod new(null!, null!) + public static MultiSubMod WithoutGroup(string name, string description, ModPriority priority) + => new(null!) { Name = name, Description = description, diff --git a/Penumbra/Mods/SubMods/OptionSubMod.cs b/Penumbra/Mods/SubMods/OptionSubMod.cs index fbf03243..02d86af2 100644 --- a/Penumbra/Mods/SubMods/OptionSubMod.cs +++ b/Penumbra/Mods/SubMods/OptionSubMod.cs @@ -1,25 +1,26 @@ -using OtterGui; -using Penumbra.Meta.Manipulations; -using Penumbra.Mods.Editor; -using Penumbra.Mods.Groups; -using Penumbra.String.Classes; - +using OtterGui; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods.Editor; +using Penumbra.Mods.Groups; +using Penumbra.String.Classes; + namespace Penumbra.Mods.SubMods; -public interface IModDataOption : IModDataContainer, IModOption; - -public abstract class OptionSubMod(Mod mod, T group) : IModDataOption - where T : IModGroup +public abstract class OptionSubMod(IModGroup group) : IModOption, IModDataContainer { - internal readonly Mod Mod = mod; - internal readonly IModGroup Group = group; + protected readonly IModGroup Group = group; - public string Name { get; set; } = "Option"; + public Mod Mod + => Group.Mod; + + public string Name { get; set; } = "Option"; + public string Description { get; set; } = string.Empty; public string FullName - => $"{Group!.Name}: {Name}"; + => $"{Group.Name}: {Name}"; - public string Description { get; set; } = string.Empty; + Mod IModOption.Mod + => Mod; IMod IModDataContainer.Mod => Mod; @@ -27,6 +28,9 @@ public abstract class OptionSubMod(Mod mod, T group) : IModDataOption IModGroup IModDataContainer.Group => Group; + IModGroup IModOption.Group + => Group; + public Dictionary Files { get; set; } = []; public Dictionary FileSwaps { get; set; } = []; public HashSet Manipulations { get; set; } = []; @@ -43,8 +47,8 @@ public abstract class OptionSubMod(Mod mod, T group) : IModDataOption public (int GroupIndex, int DataIndex) GetDataIndices() => (Group.GetIndex(), GetDataIndex()); - public (int GroupIndex, int OptionIndex) GetOptionIndices() - => (Group.GetIndex(), GetDataIndex()); + public int GetIndex() + => SubMod.GetIndex(this); private int GetDataIndex() { @@ -54,4 +58,11 @@ public abstract class OptionSubMod(Mod mod, T group) : IModDataOption return dataIndex; } -} +} + +public abstract class OptionSubMod(T group) : OptionSubMod(group) + where T : IModGroup +{ + public new T Group + => (T)base.Group; +} diff --git a/Penumbra/Mods/SubMods/SingleSubMod.cs b/Penumbra/Mods/SubMods/SingleSubMod.cs index 98c56151..675f37bc 100644 --- a/Penumbra/Mods/SubMods/SingleSubMod.cs +++ b/Penumbra/Mods/SubMods/SingleSubMod.cs @@ -4,18 +4,18 @@ using Penumbra.Mods.Settings; namespace Penumbra.Mods.SubMods; -public class SingleSubMod(Mod mod, SingleModGroup singleGroup) : OptionSubMod(mod, singleGroup) +public class SingleSubMod(SingleModGroup singleGroup) : OptionSubMod(singleGroup) { - public SingleSubMod(Mod mod, SingleModGroup singleGroup, JToken json) - : this(mod, singleGroup) + public SingleSubMod(SingleModGroup singleGroup, JToken json) + : this(singleGroup) { SubMod.LoadOptionData(json, this); - SubMod.LoadDataContainer(json, this, mod.ModPath); + SubMod.LoadDataContainer(json, this, singleGroup.Mod.ModPath); } - public SingleSubMod Clone(Mod mod, SingleModGroup group) + public SingleSubMod Clone(SingleModGroup group) { - var ret = new SingleSubMod(mod, group) + var ret = new SingleSubMod(group) { Name = Name, Description = Description, @@ -25,9 +25,9 @@ public class SingleSubMod(Mod mod, SingleModGroup singleGroup) : OptionSubMod group switch - { - SingleModGroup single => new SingleSubMod(group.Mod, single) - { - Name = name, - Description = description, - }, - MultiModGroup multi => new MultiSubMod(group.Mod, multi) - { - Name = name, - Description = description, - }, - _ => throw new ArgumentOutOfRangeException(nameof(group)), - }; + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] + public static int GetIndex(IModOption option) + { + var dataIndex = option.Group.Options.IndexOf(option); + if (dataIndex < 0) + throw new Exception($"Group {option.Group.Name} from option {option.Name} does not contain this option."); + + return dataIndex; + } /// Add all unique meta manipulations, file redirections and then file swaps from a ModDataContainer to the given sets. Skip any keys that are already contained. + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] public static void AddContainerTo(IModDataContainer container, Dictionary redirections, HashSet manipulations) { @@ -37,6 +32,7 @@ public static class SubMod } /// Replace all data of with the data of . + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] public static void Clone(IModDataContainer from, IModDataContainer to) { to.Files = new Dictionary(from.Files); @@ -45,6 +41,7 @@ public static class SubMod } /// Load all file redirections, file swaps and meta manipulations from a JToken of that option into a data container. + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] public static void LoadDataContainer(JToken json, IModDataContainer data, DirectoryInfo basePath) { data.Files.Clear(); @@ -75,6 +72,7 @@ public static class SubMod } /// Load the relevant data for a selectable option from a JToken of that option. + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] public static void LoadOptionData(JToken json, IModOption option) { option.Name = json[nameof(option.Name)]?.ToObject() ?? string.Empty; @@ -82,6 +80,7 @@ public static class SubMod } /// Write file redirections, file swaps and meta manipulations from a data container on a JsonWriter. + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] public static void WriteModContainer(JsonWriter j, JsonSerializer serializer, IModDataContainer data, DirectoryInfo basePath) { j.WritePropertyName(nameof(data.Files)); @@ -111,6 +110,7 @@ public static class SubMod } /// Write the data for a selectable mod option on a JsonWriter. + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] public static void WriteModOption(JsonWriter j, IModOption option) { j.WritePropertyName(nameof(option.Name)); diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index 0ec1fd44..2d595ec1 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -23,6 +23,12 @@ PROFILING; + + + + + + PreserveNewest @@ -93,10 +99,6 @@ - - - - diff --git a/Penumbra/Services/SaveService.cs b/Penumbra/Services/SaveService.cs index 8d3cb641..eff3295d 100644 --- a/Penumbra/Services/SaveService.cs +++ b/Penumbra/Services/SaveService.cs @@ -34,8 +34,11 @@ public sealed class SaveService(Logger log, FrameworkManager framework, Filename } } - for (var i = 0; i < mod.Groups.Count - 1; ++i) - ImmediateSave(new ModSaveGroup(mod, i, onlyAscii)); - ImmediateSaveSync(new ModSaveGroup(mod, mod.Groups.Count - 1, onlyAscii)); + if (mod.Groups.Count > 0) + { + foreach (var group in mod.Groups.SkipLast(1)) + ImmediateSave(new ModSaveGroup(group, onlyAscii)); + ImmediateSaveSync(new ModSaveGroup(mod.Groups[^1], onlyAscii)); + } } } diff --git a/Penumbra/Services/StaticServiceManager.cs b/Penumbra/Services/StaticServiceManager.cs index e758aa35..5fa1a848 100644 --- a/Penumbra/Services/StaticServiceManager.cs +++ b/Penumbra/Services/StaticServiceManager.cs @@ -121,7 +121,6 @@ public static class StaticServiceManager private static ServiceManager AddMods(this ServiceManager services) => services.AddSingleton() .AddSingleton() - .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 77bdb161..cd55beb0 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -17,6 +17,7 @@ using Penumbra.Mods.Groups; using Penumbra.Mods.ItemSwap; using Penumbra.Mods.Manager; using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; using Penumbra.Services; using Penumbra.UI.Classes; @@ -264,9 +265,10 @@ public class ItemSwapTab : IDisposable, ITab return; _modManager.AddMod(newDir); - if (!_swapData.WriteMod(_modManager, _modManager[^1], + var mod = _modManager[^1]; + if (!_swapData.WriteMod(_modManager, mod, mod.Default, _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps)) - _modManager.DeleteMod(_modManager[^1]); + _modManager.DeleteMod(mod); } private void CreateOption() @@ -276,7 +278,7 @@ public class ItemSwapTab : IDisposable, ITab var groupCreated = false; var dirCreated = false; - var optionCreated = -1; + IModOption? createdOption = null; DirectoryInfo? optionFolderName = null; try { @@ -290,22 +292,22 @@ public class ItemSwapTab : IDisposable, ITab { if (_selectedGroup == null) { - _modManager.OptionEditor.AddModGroup(_mod, GroupType.Multi, _newGroupName); - _selectedGroup = _mod.Groups.Last(); + if (_modManager.OptionEditor.AddModGroup(_mod, GroupType.Multi, _newGroupName) is not { } group) + throw new Exception($"Failure creating option group."); + + _selectedGroup = group; groupCreated = true; } - var optionIdx = _modManager.OptionEditor.AddOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _newOptionName); - if (optionIdx < 0) + if (_modManager.OptionEditor.AddOption(_selectedGroup, _newOptionName) is not { } option) throw new Exception($"Failure creating mod option."); - optionCreated = optionIdx; + createdOption = option; optionFolderName = Directory.CreateDirectory(optionFolderName.FullName); dirCreated = true; - if (!_swapData.WriteMod(_modManager, _mod, - _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps, - optionFolderName, - _mod.Groups.IndexOf(_selectedGroup), optionIdx)) + // #TODO ModOption <> DataContainer + if (!_swapData.WriteMod(_modManager, _mod, (IModDataContainer)option, + _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps, optionFolderName)) throw new Exception("Failure writing files for mod swap."); } } @@ -314,12 +316,12 @@ public class ItemSwapTab : IDisposable, ITab Penumbra.Messager.NotificationMessage(e, "Could not create new Swap Option.", NotificationType.Error, false); try { - if (optionCreated >= 0 && _selectedGroup != null) - _modManager.OptionEditor.DeleteOption(_mod, _mod.Groups.IndexOf(_selectedGroup), optionCreated); + if (createdOption != null) + _modManager.OptionEditor.DeleteOption(createdOption); if (groupCreated) { - _modManager.OptionEditor.DeleteModGroup(_mod, _mod.Groups.IndexOf(_selectedGroup!)); + _modManager.OptionEditor.DeleteModGroup(_selectedGroup!); _selectedGroup = null; } @@ -717,7 +719,8 @@ public class ItemSwapTab : IDisposable, ITab _dirty = true; } - private void OnModOptionChange(ModOptionChangeType type, Mod mod, int a, int b, int c) + private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, + int fromIdx) { if (type is ModOptionChangeType.PrepareChange or ModOptionChangeType.GroupAdded or ModOptionChangeType.OptionAdded || mod != _mod) return; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 92a9dd66..743310ea 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -27,7 +27,7 @@ public partial class ModEditWindow private const string GenderTooltip = "Gender"; private const string ObjectTypeTooltip = "Object Type"; private const string SecondaryIdTooltip = "Secondary ID"; - private const string PrimaryIDTooltip = "Primary ID"; + private const string PrimaryIdTooltipShort = "Primary ID"; private const string VariantIdTooltip = "Variant ID"; private const string EstTypeTooltip = "EST Type"; private const string RacialTribeTooltip = "Racial Tribe"; @@ -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.DataIdx); + _editor.MetaEditor.Apply(_editor.Option!); ImGui.SameLine(); tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; @@ -477,7 +477,7 @@ public partial class ModEditWindow ImGui.TableNextColumn(); ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); ImGui.TextUnformatted(meta.PrimaryId.ToString()); - ImGuiUtil.HoverTooltip(PrimaryIDTooltip); + ImGuiUtil.HoverTooltip(PrimaryIdTooltipShort); ImGui.TableNextColumn(); ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index dbb88fb7..6b48a048 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -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.DataIdx); + _editor.SwapEditor.Apply(_editor.Option!); ImGui.SameLine(); tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; @@ -627,7 +627,7 @@ public partial class ModEditWindow : Window, IDisposable public void Dispose() { _communicator.ModPathChanged.Unsubscribe(OnModPathChange); - _editor?.Dispose(); + _editor.Dispose(); _materialTab.Dispose(); _modelTab.Dispose(); _shaderPackageTab.Dispose(); diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index afbef45d..fcd76a51 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -15,6 +15,7 @@ using Penumbra.Services; using Penumbra.UI.AdvancedWindow; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; +using Penumbra.Mods.SubMods; namespace Penumbra.UI.ModsTab; @@ -248,13 +249,13 @@ public class ModPanelEditTab( ImGui.SameLine(); - var nameValid = ModOptionEditor.VerifyFileName(mod, null, _newGroupName, false); + var nameValid = ModGroupEditor.VerifyFileName(mod, null, _newGroupName, false); tt = nameValid ? "Add new option group to the mod." : "Can not add a group of this name."; if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, tt, !nameValid, true)) return; - modManager.OptionEditor.AddModGroup(mod, GroupType.Single, _newGroupName); + modManager.OptionEditor.SingleEditor.AddModGroup(mod, _newGroupName); Reset(); } } @@ -364,9 +365,9 @@ public class ModPanelEditTab( break; case >= 0: if (_newDescriptionOptionIdx < 0) - modManager.OptionEditor.ChangeGroupDescription(_mod, _newDescriptionIdx, _newDescription); + modManager.OptionEditor.ChangeGroupDescription(_mod.Groups[_newDescriptionIdx], _newDescription); else - modManager.OptionEditor.ChangeOptionDescription(_mod, _newDescriptionIdx, _newDescriptionOptionIdx, + modManager.OptionEditor.ChangeOptionDescription(_mod.Groups[_newDescriptionIdx].Options[_newDescriptionOptionIdx], _newDescription); break; @@ -396,18 +397,18 @@ public class ModPanelEditTab( .Push(ImGuiStyleVar.ItemSpacing, _itemSpacing); if (Input.Text("##Name", groupIdx, Input.None, group.Name, out var newGroupName, 256, UiHelpers.InputTextWidth.X)) - _modManager.OptionEditor.RenameModGroup(_mod, groupIdx, newGroupName); + _modManager.OptionEditor.RenameModGroup(group, newGroupName); ImGuiUtil.HoverTooltip("Group Name"); ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, "Delete this option group.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true)) - _delayedActions.Enqueue(() => _modManager.OptionEditor.DeleteModGroup(_mod, groupIdx)); + _delayedActions.Enqueue(() => _modManager.OptionEditor.DeleteModGroup(group)); ImGui.SameLine(); if (Input.Priority("##Priority", groupIdx, Input.None, group.Priority, out var priority, 50 * UiHelpers.Scale)) - _modManager.OptionEditor.ChangeGroupPriority(_mod, groupIdx, priority); + _modManager.OptionEditor.ChangeGroupPriority(group, priority); ImGuiUtil.HoverTooltip("Group Priority"); @@ -417,7 +418,7 @@ public class ModPanelEditTab( var tt = groupIdx == 0 ? "Can not move this group further upwards." : $"Move this group up to group {groupIdx}."; if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowUp.ToIconString(), UiHelpers.IconButtonSize, tt, groupIdx == 0, true)) - _delayedActions.Enqueue(() => _modManager.OptionEditor.MoveModGroup(_mod, groupIdx, groupIdx - 1)); + _delayedActions.Enqueue(() => _modManager.OptionEditor.MoveModGroup(group, groupIdx - 1)); ImGui.SameLine(); tt = groupIdx == _mod.Groups.Count - 1 @@ -425,7 +426,7 @@ public class ModPanelEditTab( : $"Move this group down to group {groupIdx + 2}."; if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowDown.ToIconString(), UiHelpers.IconButtonSize, tt, groupIdx == _mod.Groups.Count - 1, true)) - _delayedActions.Enqueue(() => _modManager.OptionEditor.MoveModGroup(_mod, groupIdx, groupIdx + 1)); + _delayedActions.Enqueue(() => _modManager.OptionEditor.MoveModGroup(group, groupIdx + 1)); ImGui.SameLine(); @@ -452,17 +453,17 @@ public class ModPanelEditTab( { private const string DragDropLabel = "##DragOption"; - private static int _newOptionNameIdx = -1; - private static string _newOptionName = string.Empty; - private static int _dragDropGroupIdx = -1; - private static int _dragDropOptionIdx = -1; + private static int _newOptionNameIdx = -1; + private static string _newOptionName = string.Empty; + private static IModGroup? _dragDropGroup; + private static IModOption? _dragDropOption; public static void Reset() { - _newOptionNameIdx = -1; - _newOptionName = string.Empty; - _dragDropGroupIdx = -1; - _dragDropOptionIdx = -1; + _newOptionNameIdx = -1; + _newOptionName = string.Empty; + _dragDropGroup = null; + _dragDropOption = null; } public static void Draw(ModPanelEditTab panel, int groupIdx) @@ -482,7 +483,7 @@ public class ModPanelEditTab( switch (panel._mod.Groups[groupIdx]) { - case SingleModGroup single: + case SingleModGroup single: for (var optionIdx = 0; optionIdx < single.OptionData.Count; ++optionIdx) EditOption(panel, single, groupIdx, optionIdx); break; @@ -491,6 +492,7 @@ public class ModPanelEditTab( EditOption(panel, multi, groupIdx, optionIdx); break; } + DrawNewOption(panel, groupIdx, UiHelpers.IconButtonSize); } @@ -502,8 +504,8 @@ public class ModPanelEditTab( ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); ImGui.Selectable($"Option #{optionIdx + 1}"); - Source(group, groupIdx, optionIdx); - Target(panel, group, groupIdx, optionIdx); + Source(option); + Target(panel, group, optionIdx); ImGui.TableNextColumn(); @@ -511,7 +513,7 @@ public class ModPanelEditTab( if (group.Type == GroupType.Single) { if (ImGui.RadioButton("##default", group.DefaultSettings.AsIndex == optionIdx)) - panel._modManager.OptionEditor.ChangeModGroupDefaultOption(panel._mod, groupIdx, Setting.Single(optionIdx)); + panel._modManager.OptionEditor.ChangeModGroupDefaultOption(group, Setting.Single(optionIdx)); ImGuiUtil.HoverTooltip($"Set {option.Name} as the default choice for this group."); } @@ -519,15 +521,14 @@ public class ModPanelEditTab( { var isDefaultOption = group.DefaultSettings.HasFlag(optionIdx); if (ImGui.Checkbox("##default", ref isDefaultOption)) - panel._modManager.OptionEditor.ChangeModGroupDefaultOption(panel._mod, groupIdx, - group.DefaultSettings.SetBit(optionIdx, isDefaultOption)); + panel._modManager.OptionEditor.ChangeModGroupDefaultOption(group, group.DefaultSettings.SetBit(optionIdx, isDefaultOption)); ImGuiUtil.HoverTooltip($"{(isDefaultOption ? "Disable" : "Enable")} {option.Name} per default in this group."); } ImGui.TableNextColumn(); if (Input.Text("##Name", groupIdx, optionIdx, option.Name, out var newOptionName, 256, -1)) - panel._modManager.OptionEditor.RenameOption(panel._mod, groupIdx, optionIdx, newOptionName); + panel._modManager.OptionEditor.RenameOption(option, newOptionName); ImGui.TableNextColumn(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Edit.ToIconString(), UiHelpers.IconButtonSize, "Edit option description.", @@ -537,15 +538,15 @@ public class ModPanelEditTab( ImGui.TableNextColumn(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, "Delete this option.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true)) - panel._delayedActions.Enqueue(() => panel._modManager.OptionEditor.DeleteOption(panel._mod, groupIdx, optionIdx)); + panel._delayedActions.Enqueue(() => panel._modManager.OptionEditor.DeleteOption(option)); ImGui.TableNextColumn(); - if (group is not MultiModGroup multi) + if (option is not MultiSubMod multi) return; - if (Input.Priority("##Priority", groupIdx, optionIdx, multi.OptionData[optionIdx].Priority, out var priority, + if (Input.Priority("##Priority", groupIdx, optionIdx, multi.Priority, out var priority, 50 * UiHelpers.Scale)) - panel._modManager.OptionEditor.ChangeOptionPriority(panel._mod, groupIdx, optionIdx, priority); + panel._modManager.OptionEditor.MultiEditor.ChangeOptionPriority(multi, priority); ImGuiUtil.HoverTooltip("Option priority."); } @@ -564,7 +565,7 @@ public class ModPanelEditTab( ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); ImGui.Selectable($"Option #{count + 1}"); - Target(panel, group, groupIdx, count); + Target(panel, group, count); ImGui.TableNextColumn(); ImGui.TableNextColumn(); ImGui.SetNextItemWidth(-1); @@ -585,14 +586,14 @@ public class ModPanelEditTab( tt, !(canAddGroup && validName), true)) return; - panel._modManager.OptionEditor.AddOption(mod, groupIdx, _newOptionName); + panel._modManager.OptionEditor.AddOption(group, _newOptionName); _newOptionName = string.Empty; } // Handle drag and drop to move options inside a group or into another group. - private static void Source(IModGroup group, int groupIdx, int optionIdx) + private static void Source(IModOption option) { - if (group is not ITexToolsGroup) + if (option.Group is not ITexToolsGroup) return; using var source = ImRaii.DragDropSource(); @@ -601,14 +602,14 @@ public class ModPanelEditTab( if (ImGui.SetDragDropPayload(DragDropLabel, IntPtr.Zero, 0)) { - _dragDropGroupIdx = groupIdx; - _dragDropOptionIdx = optionIdx; + _dragDropGroup = option.Group; + _dragDropOption = option; } - ImGui.TextUnformatted($"Dragging option {group.Options[optionIdx].Name} from group {group.Name}..."); + ImGui.TextUnformatted($"Dragging option {option.Name} from group {option.Group.Name}..."); } - private static void Target(ModPanelEditTab panel, IModGroup group, int groupIdx, int optionIdx) + private static void Target(ModPanelEditTab panel, IModGroup group, int optionIdx) { if (group is not ITexToolsGroup) return; @@ -617,39 +618,53 @@ public class ModPanelEditTab( if (!target.Success || !ImGuiUtil.IsDropping(DragDropLabel)) return; - if (_dragDropGroupIdx >= 0 && _dragDropOptionIdx >= 0) + if (_dragDropGroup != null && _dragDropOption != null) { - if (_dragDropGroupIdx == groupIdx) + if (_dragDropGroup == group) { - var sourceOption = _dragDropOptionIdx; + var sourceOption = _dragDropOption; panel._delayedActions.Enqueue( - () => panel._modManager.OptionEditor.MoveOption(panel._mod, groupIdx, sourceOption, optionIdx)); + () => panel._modManager.OptionEditor.MoveOption(sourceOption, optionIdx)); } else { // Move from one group to another by deleting, then adding, then moving the option. - var sourceGroupIdx = _dragDropGroupIdx; - var sourceOption = _dragDropOptionIdx; - var sourceGroup = panel._mod.Groups[sourceGroupIdx]; - var currentCount = group.DataContainers.Count; - var option = ((ITexToolsGroup) sourceGroup).OptionData[_dragDropOptionIdx]; + var sourceOption = _dragDropOption; panel._delayedActions.Enqueue(() => { - panel._modManager.OptionEditor.DeleteOption(panel._mod, sourceGroupIdx, sourceOption); - panel._modManager.OptionEditor.AddOption(panel._mod, groupIdx, option); - panel._modManager.OptionEditor.MoveOption(panel._mod, groupIdx, currentCount, optionIdx); + panel._modManager.OptionEditor.DeleteOption(sourceOption); + if (panel._modManager.OptionEditor.AddOption(group, sourceOption) is { } newOption) + panel._modManager.OptionEditor.MoveOption(newOption, optionIdx); }); } } - _dragDropGroupIdx = -1; - _dragDropOptionIdx = -1; + _dragDropGroup = null; + _dragDropOption = null; } } /// Draw a combo to select single or multi group and switch between them. private void DrawGroupCombo(IModGroup group, int groupIdx) { + ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X - 2 * UiHelpers.IconButtonSize.X - 2 * ImGui.GetStyle().ItemSpacing.X); + using var combo = ImRaii.Combo("##GroupType", GroupTypeName(group.Type)); + if (!combo) + return; + + if (ImGui.Selectable(GroupTypeName(GroupType.Single), group.Type == GroupType.Single) && group is MultiModGroup m) + _modManager.OptionEditor.MultiEditor.ChangeToSingle(m); + + var canSwitchToMulti = group.Options.Count <= IModGroup.MaxMultiOptions; + using var style = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, !canSwitchToMulti); + if (ImGui.Selectable(GroupTypeName(GroupType.Multi), group.Type == GroupType.Multi) && canSwitchToMulti && group is SingleModGroup s) + _modManager.OptionEditor.SingleEditor.ChangeToMulti(s); + + style.Pop(); + if (!canSwitchToMulti) + ImGuiUtil.HoverTooltip($"Can not convert group to multi group since it has more than {IModGroup.MaxMultiOptions} options."); + return; + static string GroupTypeName(GroupType type) => type switch { @@ -657,23 +672,6 @@ public class ModPanelEditTab( GroupType.Multi => "Multi Group", _ => "Unknown", }; - - ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X - 2 * UiHelpers.IconButtonSize.X - 2 * ImGui.GetStyle().ItemSpacing.X); - using var combo = ImRaii.Combo("##GroupType", GroupTypeName(group.Type)); - if (!combo) - return; - - if (ImGui.Selectable(GroupTypeName(GroupType.Single), group.Type == GroupType.Single)) - _modManager.OptionEditor.ChangeModGroupType(_mod, groupIdx, GroupType.Single); - - var canSwitchToMulti = group.Options.Count <= IModGroup.MaxMultiOptions; - using var style = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, !canSwitchToMulti); - if (ImGui.Selectable(GroupTypeName(GroupType.Multi), group.Type == GroupType.Multi) && canSwitchToMulti) - _modManager.OptionEditor.ChangeModGroupType(_mod, groupIdx, GroupType.Multi); - - style.Pop(); - if (!canSwitchToMulti) - ImGuiUtil.HoverTooltip($"Can not convert group to multi group since it has more than {IModGroup.MaxMultiOptions} options."); } /// Handles input text and integers in separate fields without buffers for every single one.