From fbe2ed1a7117ec812a495acfdfdc2a6010b5d2dd Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Mon, 27 Mar 2023 17:09:19 +0200 Subject: [PATCH] Bunch of work on Option Editor. --- Penumbra/Collections/CollectionManager.cs | 21 +- Penumbra/Mods/Editor/DuplicateManager.cs | 18 +- Penumbra/Mods/Editor/ModFileEditor.cs | 4 +- Penumbra/Mods/Editor/ModMetaEditor.cs | 2 +- Penumbra/Mods/Editor/ModNormalizer.cs | 7 +- Penumbra/Mods/Editor/ModSwapEditor.cs | 10 +- Penumbra/Mods/ItemSwap/ItemSwapContainer.cs | 8 +- Penumbra/Mods/Manager/Mod.Manager.cs | 113 ++- ....Manager.Options.cs => ModOptionEditor.cs} | 763 +++++++++--------- Penumbra/Mods/Mod.BasePath.cs | 2 +- Penumbra/Mods/Mod.Creator.cs | 8 +- Penumbra/Mods/Mod.Files.cs | 29 +- Penumbra/Mods/Mod.Meta.Migration.cs | 4 +- Penumbra/Mods/Subclasses/IModGroup.cs | 135 ++-- Penumbra/Penumbra.cs | 69 +- Penumbra/PenumbraNew.cs | 1 + Penumbra/Services/CommunicatorService.cs | 12 +- Penumbra/Services/FilenameService.cs | 17 + Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 12 +- Penumbra/UI/ModsTab/ModPanelEditTab.cs | 88 +- Penumbra/Util/SaveService.cs | 21 + 21 files changed, 749 insertions(+), 595 deletions(-) rename Penumbra/Mods/Manager/{Mod.Manager.Options.cs => ModOptionEditor.cs} (54%) diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index d03d3ddf..c1510686 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -19,8 +19,9 @@ namespace Penumbra.Collections; public sealed partial class CollectionManager : IDisposable, IEnumerable { - private readonly Mods.ModManager _modManager; + private readonly ModManager _modManager; private readonly CommunicatorService _communicator; + private readonly SaveService _saveService; private readonly CharacterUtility _characterUtility; private readonly ResidentResourceManager _residentResources; private readonly Configuration _config; @@ -57,7 +58,8 @@ public sealed partial class CollectionManager : IDisposable, IEnumerable _collections; public CollectionManager(StartTracker timer, CommunicatorService communicator, FilenameService files, CharacterUtility characterUtility, - ResidentResourceManager residentResources, Configuration config, Mods.ModManager modManager, IndividualCollections individuals) + ResidentResourceManager residentResources, Configuration config, ModManager modManager, IndividualCollections individuals, + SaveService saveService) { using var time = timer.Measure(StartTimeType.Collections); _communicator = communicator; @@ -65,12 +67,13 @@ public sealed partial class CollectionManager : IDisposable, IEnumerable _duplicates = new(); - public DuplicateManager(ModFileCollection files, ModManager modManager) + public DuplicateManager(ModFileCollection files, ModManager modManager, SaveService saveService) { - _files = files; - _modManager = modManager; + _files = files; + _modManager = modManager; + _saveService = saveService; } public IReadOnlyList<(FullPath[] Paths, long Size, byte[] Hash)> Duplicates @@ -76,16 +79,13 @@ public class DuplicateManager if (useModManager) { - _modManager.OptionSetFiles(mod, groupIdx, optionIdx, dict); + _modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, dict); } else { var sub = (SubMod)subMod; sub.FileData = dict; - if (groupIdx == -1) - mod.SaveDefaultMod(); - else - IModGroup.Save(mod.Groups[groupIdx], mod.ModPath, groupIdx); + _saveService.ImmediateSave(new ModSaveGroup(mod, groupIdx)); } } diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index 29a06c44..d5e5fea6 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -34,7 +34,7 @@ public class ModFileEditor num += dict.TryAdd(path.Item2, file.File) ? 0 : 1; } - Penumbra.ModManager.OptionSetFiles(mod, option.GroupIdx, option.OptionIdx, dict); + _modManager.OptionEditor.OptionSetFiles(mod, option.GroupIdx, option.OptionIdx, dict); _files.UpdatePaths(mod, option); return num; @@ -54,7 +54,7 @@ public class ModFileEditor 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.OptionSetFiles(mod, groupIdx, optionIdx, newDict); + _modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, newDict); } ModEditor.ApplyToAllOptions(mod, HandleSubMod); diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index f536935d..b1dced58 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -109,7 +109,7 @@ public class ModMetaEditor if (!Changes) return; - _modManager.OptionSetManipulations(mod, groupIdx, optionIdx, Recombine().ToHashSet()); + _modManager.OptionEditor.OptionSetManipulations(mod, groupIdx, optionIdx, Recombine().ToHashSet()); Changes = false; } diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index aff491c7..bdd968cd 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -11,7 +11,7 @@ namespace Penumbra.Mods; public class ModNormalizer { - private readonly ModManager _modManager; + private readonly ModManager _modManager; private readonly List>> _redirections = new(); public Mod Mod { get; private set; } = null!; @@ -280,9 +280,8 @@ public class ModNormalizer private void ApplyRedirections() { foreach (var option in Mod.AllSubMods.OfType()) - { - _modManager.OptionSetFiles(Mod, option.GroupIdx, option.OptionIdx, _redirections[option.GroupIdx + 1][option.OptionIdx]); - } + _modManager.OptionEditor.OptionSetFiles(Mod, option.GroupIdx, option.OptionIdx, + _redirections[option.GroupIdx + 1][option.OptionIdx]); ++Step; } diff --git a/Penumbra/Mods/Editor/ModSwapEditor.cs b/Penumbra/Mods/Editor/ModSwapEditor.cs index 29da93c1..e411ad70 100644 --- a/Penumbra/Mods/Editor/ModSwapEditor.cs +++ b/Penumbra/Mods/Editor/ModSwapEditor.cs @@ -22,11 +22,11 @@ public class ModSwapEditor public void Apply(Mod mod, int groupIdx, int optionIdx) { - if (Changes) - { - _modManager.OptionSetFileSwaps(mod, groupIdx, optionIdx, _swaps); - Changes = false; - } + if (!Changes) + return; + + _modManager.OptionEditor.OptionSetFileSwaps(mod, groupIdx, optionIdx, _swaps); + Changes = false; } public bool Changes { get; private set; } diff --git a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs index 6b6e3111..964aee70 100644 --- a/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs +++ b/Penumbra/Mods/ItemSwap/ItemSwapContainer.cs @@ -38,7 +38,7 @@ public class ItemSwapContainer NoSwaps, } - public bool WriteMod( Mod mod, WriteType writeType = WriteType.NoSwaps, DirectoryInfo? directory = null, int groupIndex = -1, int optionIndex = 0 ) + public bool WriteMod( ModManager manager, Mod mod, WriteType writeType = WriteType.NoSwaps, DirectoryInfo? directory = null, int groupIndex = -1, int optionIndex = 0 ) { var convertedManips = new HashSet< MetaManipulation >( Swaps.Count ); var convertedFiles = new Dictionary< Utf8GamePath, FullPath >( Swaps.Count ); @@ -82,9 +82,9 @@ public class ItemSwapContainer } } - Penumbra.ModManager.OptionSetFiles( mod, groupIndex, optionIndex, convertedFiles ); - Penumbra.ModManager.OptionSetFileSwaps( mod, groupIndex, optionIndex, convertedSwaps ); - Penumbra.ModManager.OptionSetManipulations( mod, groupIndex, optionIndex, convertedManips ); + manager.OptionEditor.OptionSetFiles( mod, groupIndex, optionIndex, convertedFiles ); + manager.OptionEditor.OptionSetFileSwaps( mod, groupIndex, optionIndex, convertedSwaps ); + manager.OptionEditor.OptionSetManipulations( mod, groupIndex, optionIndex, convertedManips ); return true; } catch( Exception e ) diff --git a/Penumbra/Mods/Manager/Mod.Manager.cs b/Penumbra/Mods/Manager/Mod.Manager.cs index dd40e9d4..7e3310e4 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.cs @@ -2,12 +2,76 @@ using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using Penumbra.Services; using Penumbra.Util; namespace Penumbra.Mods; -public sealed partial class ModManager : IReadOnlyList +public sealed class ModManager2 : IReadOnlyList, IDisposable +{ + public readonly ModDataEditor DataEditor; + public readonly ModOptionEditor OptionEditor; + + /// + /// An easily accessible set of new mods. + /// Mods are added when they are created or imported. + /// Mods are removed when they are deleted or when they are toggled in any collection. + /// Also gets cleared on mod rediscovery. + /// + public readonly HashSet NewMods = new(); + + public Mod this[int idx] + => _mods[idx]; + + public Mod this[Index idx] + => _mods[idx]; + + public int Count + => _mods.Count; + + public IEnumerator GetEnumerator() + => _mods.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + /// + /// Try to obtain a mod by its directory name (unique identifier, preferred), + /// or the first mod of the given name if no directory fits. + /// + public bool TryGetMod(string identifier, string modName, [NotNullWhen(true)] out Mod? mod) + { + mod = null; + foreach (var m in _mods) + { + if (string.Equals(m.Identifier, identifier, StringComparison.OrdinalIgnoreCase)) + { + mod = m; + return true; + } + + if (m.Name == modName) + mod ??= m; + } + + return mod != null; + } + + /// The actual list of mods. + private readonly List _mods = new(); + + public ModManager2(ModDataEditor dataEditor, ModOptionEditor optionEditor) + { + DataEditor = dataEditor; + OptionEditor = optionEditor; + } + + public void Dispose() + { } +} + +public sealed partial class ModManager : IReadOnlyList, IDisposable { // Set when reading Config and migrating from v4 to v5. public static bool MigrateModBackups = false; @@ -38,21 +102,29 @@ public sealed partial class ModManager : IReadOnlyList private readonly Configuration _config; private readonly CommunicatorService _communicator; public readonly ModDataEditor DataEditor; + public readonly ModOptionEditor OptionEditor; - public ModManager(StartTracker time, Configuration config, CommunicatorService communicator, ModDataEditor dataEditor) + public ModManager(StartTracker time, Configuration config, CommunicatorService communicator, ModDataEditor dataEditor, + ModOptionEditor optionEditor) { using var timer = time.Measure(StartTimeType.Mods); _config = config; _communicator = communicator; DataEditor = dataEditor; + OptionEditor = optionEditor; ModDirectoryChanged += OnModDirectoryChange; SetBaseDirectory(config.ModDirectory, true); UpdateExportDirectory(_config.ExportDirectory, false); - ModOptionChanged += OnModOptionChange; - ModPathChanged += OnModPathChange; + _communicator.ModOptionChanged.Event += OnModOptionChange; + ModPathChanged += OnModPathChange; DiscoverMods(); } + public void Dispose() + { + _communicator.ModOptionChanged.Event -= OnModOptionChange; + } + // Try to obtain a mod by its directory name (unique identifier, preferred), // or the first mod of the given name if no directory fits. @@ -73,4 +145,37 @@ public sealed partial class ModManager : IReadOnlyList return mod != null; } + + private static void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int _, int _2) + { + if (type == ModOptionChangeType.PrepareChange) + return; + + bool ComputeChangedItems() + { + mod.ComputeChangedItems(); + return true; + } + + // State can not change on adding groups, as they have no immediate options. + var unused = type switch + { + ModOptionChangeType.GroupAdded => ComputeChangedItems() & mod.SetCounts(), + ModOptionChangeType.GroupDeleted => ComputeChangedItems() & mod.SetCounts(), + ModOptionChangeType.GroupMoved => false, + ModOptionChangeType.GroupTypeChanged => mod.HasOptions = mod.Groups.Any(o => o.IsOption), + ModOptionChangeType.PriorityChanged => false, + ModOptionChangeType.OptionAdded => ComputeChangedItems() & mod.SetCounts(), + ModOptionChangeType.OptionDeleted => ComputeChangedItems() & mod.SetCounts(), + ModOptionChangeType.OptionMoved => false, + ModOptionChangeType.OptionFilesChanged => ComputeChangedItems() + & (0 < (mod.TotalFileCount = mod.AllSubMods.Sum(s => s.Files.Count))), + ModOptionChangeType.OptionSwapsChanged => ComputeChangedItems() + & (0 < (mod.TotalSwapCount = mod.AllSubMods.Sum(s => s.FileSwaps.Count))), + ModOptionChangeType.OptionMetaChanged => ComputeChangedItems() + & (0 < (mod.TotalManipulations = mod.AllSubMods.Sum(s => s.Manipulations.Count))), + ModOptionChangeType.DisplayChange => false, + _ => false, + }; + } } diff --git a/Penumbra/Mods/Manager/Mod.Manager.Options.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs similarity index 54% rename from Penumbra/Mods/Manager/Mod.Manager.Options.cs rename to Penumbra/Mods/Manager/ModOptionEditor.cs index 0c86e82d..974dd837 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Options.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -1,377 +1,386 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Dalamud.Interface.Internal.Notifications; -using OtterGui; -using OtterGui.Filesystem; -using Penumbra.Api.Enums; -using Penumbra.Meta.Manipulations; -using Penumbra.String.Classes; -using Penumbra.Util; - -namespace Penumbra.Mods; - -public sealed partial class ModManager -{ - public delegate void ModOptionChangeDelegate(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx); - public event ModOptionChangeDelegate ModOptionChanged; - - 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); - ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1); - } - - public void ChangeModGroupDefaultOption(Mod mod, int groupIdx, uint defaultOption) - { - var group = mod._groups[groupIdx]; - if (group.DefaultSettings == defaultOption) - return; - - group.DefaultSettings = defaultOption; - ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, mod, groupIdx, -1, -1); - } - - 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; - - group.DeleteFile(mod.ModPath, groupIdx); - - var _ = group switch - { - SingleModGroup s => s.Name = newName, - MultiModGroup m => m.Name = newName, - _ => newName, - }; - - ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1); - } - - public void AddModGroup(Mod mod, GroupType type, string newName) - { - if (!VerifyFileName(mod, null, newName, true)) - return; - - var maxPriority = mod._groups.Count == 0 ? 0 : mod._groups.Max(o => o.Priority) + 1; - - mod._groups.Add(type == GroupType.Multi - ? new MultiModGroup - { - Name = newName, - Priority = maxPriority, - } - : new SingleModGroup - { - Name = newName, - Priority = maxPriority, - }); - ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod._groups.Count - 1, -1, -1); - } - - public void DeleteModGroup(Mod mod, int groupIdx) - { - var group = mod._groups[groupIdx]; - ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1); - mod._groups.RemoveAt(groupIdx); - UpdateSubModPositions(mod, groupIdx); - group.DeleteFile(mod.ModPath, groupIdx); - ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1); - } - - public void MoveModGroup(Mod mod, int groupIdxFrom, int groupIdxTo) - { - if (mod._groups.Move(groupIdxFrom, groupIdxTo)) - { - UpdateSubModPositions(mod, Math.Min(groupIdxFrom, groupIdxTo)); - ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo); - } - } - - private static void UpdateSubModPositions(Mod mod, int fromGroup) - { - foreach (var (group, groupIdx) in mod._groups.WithIndex().Skip(fromGroup)) - { - foreach (var (o, optionIdx) in group.OfType().WithIndex()) - o.SetPosition(groupIdx, optionIdx); - } - } - - public void ChangeGroupDescription(Mod mod, int groupIdx, string newDescription) - { - var group = mod._groups[groupIdx]; - if (group.Description == newDescription) - return; - - var _ = group switch - { - SingleModGroup s => s.Description = newDescription, - MultiModGroup m => m.Description = newDescription, - _ => newDescription, - }; - ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, -1, -1); - } - - public void ChangeOptionDescription(Mod mod, int groupIdx, int optionIdx, string newDescription) - { - var group = mod._groups[groupIdx]; - var option = group[optionIdx]; - if (option.Description == newDescription || option is not SubMod s) - return; - - s.Description = newDescription; - ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); - } - - public void ChangeGroupPriority(Mod mod, int groupIdx, int newPriority) - { - var group = mod._groups[groupIdx]; - if (group.Priority == newPriority) - return; - - var _ = group switch - { - SingleModGroup s => s.Priority = newPriority, - MultiModGroup m => m.Priority = newPriority, - _ => newPriority, - }; - ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1); - } - - public void ChangeOptionPriority(Mod mod, int groupIdx, int optionIdx, int newPriority) - { - switch (mod._groups[groupIdx]) - { - case SingleModGroup: - ChangeGroupPriority(mod, groupIdx, newPriority); - break; - case MultiModGroup m: - if (m.PrioritizedOptions[optionIdx].Priority == newPriority) - return; - - m.PrioritizedOptions[optionIdx] = (m.PrioritizedOptions[optionIdx].Mod, newPriority); - ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1); - return; - } - } - - public void RenameOption(Mod mod, int groupIdx, int optionIdx, string newName) - { - switch (mod._groups[groupIdx]) - { - case SingleModGroup s: - if (s.OptionData[optionIdx].Name == newName) - return; - - s.OptionData[optionIdx].Name = newName; - break; - case MultiModGroup m: - var option = m.PrioritizedOptions[optionIdx].Mod; - if (option.Name == newName) - return; - - option.Name = newName; - break; - } - - ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); - } - - public void AddOption(Mod mod, int groupIdx, string newName) - { - var group = mod._groups[groupIdx]; - var subMod = new SubMod(mod) { Name = newName }; - subMod.SetPosition(groupIdx, group.Count); - switch (group) - { - case SingleModGroup s: - s.OptionData.Add(subMod); - break; - case MultiModGroup m: - m.PrioritizedOptions.Add((subMod, 0)); - break; - } - - ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); - } - - public void AddOption(Mod mod, int groupIdx, ISubMod option, int priority = 0) - { - if (option is not SubMod o) - return; - - var group = mod._groups[groupIdx]; - if (group.Count > 63) - { - Penumbra.Log.Error( - $"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, " - + "since only up to 64 options are supported in one group."); - return; - } - - o.SetPosition(groupIdx, group.Count); - - switch (group) - { - case SingleModGroup s: - s.OptionData.Add(o); - break; - case MultiModGroup m: - m.PrioritizedOptions.Add((o, priority)); - break; - } - - ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); - } - - public void DeleteOption(Mod mod, int groupIdx, int optionIdx) - { - var group = mod._groups[groupIdx]; - ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); - switch (group) - { - case SingleModGroup s: - s.OptionData.RemoveAt(optionIdx); - - break; - case MultiModGroup m: - m.PrioritizedOptions.RemoveAt(optionIdx); - break; - } - - group.UpdatePositions(optionIdx); - ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1); - } - - public void MoveOption(Mod mod, int groupIdx, int optionIdxFrom, int optionIdxTo) - { - var group = mod._groups[groupIdx]; - if (group.MoveOption(optionIdxFrom, optionIdxTo)) - ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, mod, groupIdx, optionIdxFrom, optionIdxTo); - } - - public void OptionSetManipulations(Mod mod, int groupIdx, int optionIdx, HashSet manipulations) - { - var subMod = GetSubMod(mod, groupIdx, optionIdx); - if (subMod.Manipulations.Count == manipulations.Count - && subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m))) - return; - - ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); - subMod.ManipulationData = manipulations; - ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1); - } - - public void OptionSetFiles(Mod mod, int groupIdx, int optionIdx, Dictionary replacements) - { - var subMod = GetSubMod(mod, groupIdx, optionIdx); - if (subMod.FileData.SetEquals(replacements)) - return; - - ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); - subMod.FileData = replacements; - ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1); - } - - public void OptionAddFiles(Mod mod, int groupIdx, int optionIdx, Dictionary additions) - { - var subMod = GetSubMod(mod, groupIdx, optionIdx); - var oldCount = subMod.FileData.Count; - subMod.FileData.AddFrom(additions); - if (oldCount != subMod.FileData.Count) - ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, optionIdx, -1); - } - - public void OptionSetFileSwaps(Mod mod, int groupIdx, int optionIdx, Dictionary swaps) - { - var subMod = GetSubMod(mod, groupIdx, optionIdx); - if (subMod.FileSwapData.SetEquals(swaps)) - return; - - ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); - subMod.FileSwapData = swaps; - ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1); - } - - public 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.ChatService.NotificationMessage( - $"Could not name option {newName} because option with same filename {path} already exists.", - "Warning", NotificationType.Warning); - - return false; - } - - private static SubMod GetSubMod(Mod mod, int groupIdx, int optionIdx) - { - if (groupIdx == -1 && optionIdx == 0) - return mod._default; - - return mod._groups[groupIdx] switch - { - SingleModGroup s => s.OptionData[optionIdx], - MultiModGroup m => m.PrioritizedOptions[optionIdx].Mod, - _ => throw new InvalidOperationException(), - }; - } - - private static void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int _, int _2) - { - if (type == ModOptionChangeType.PrepareChange) - return; - - // File deletion is handled in the actual function. - if (type is ModOptionChangeType.GroupDeleted or ModOptionChangeType.GroupMoved) - { - mod.SaveAllGroups(); - } - else - { - if (groupIdx == -1) - mod.SaveDefaultModDelayed(); - else - IModGroup.SaveDelayed(mod._groups[groupIdx], mod.ModPath, groupIdx); - } - - bool ComputeChangedItems() - { - mod.ComputeChangedItems(); - return true; - } - - // State can not change on adding groups, as they have no immediate options. - var unused = type switch - { - ModOptionChangeType.GroupAdded => ComputeChangedItems() & mod.SetCounts(), - ModOptionChangeType.GroupDeleted => ComputeChangedItems() & mod.SetCounts(), - ModOptionChangeType.GroupMoved => false, - ModOptionChangeType.GroupTypeChanged => mod.HasOptions = mod.Groups.Any(o => o.IsOption), - ModOptionChangeType.PriorityChanged => false, - ModOptionChangeType.OptionAdded => ComputeChangedItems() & mod.SetCounts(), - ModOptionChangeType.OptionDeleted => ComputeChangedItems() & mod.SetCounts(), - ModOptionChangeType.OptionMoved => false, - ModOptionChangeType.OptionFilesChanged => ComputeChangedItems() - & (0 < (mod.TotalFileCount = mod.AllSubMods.Sum(s => s.Files.Count))), - ModOptionChangeType.OptionSwapsChanged => ComputeChangedItems() - & (0 < (mod.TotalSwapCount = mod.AllSubMods.Sum(s => s.FileSwaps.Count))), - ModOptionChangeType.OptionMetaChanged => ComputeChangedItems() - & (0 < (mod.TotalManipulations = mod.AllSubMods.Sum(s => s.Manipulations.Count))), - ModOptionChangeType.DisplayChange => false, - _ => false, - }; - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Interface.Internal.Notifications; +using OtterGui; +using OtterGui.Filesystem; +using Penumbra.Api.Enums; +using Penumbra.Meta.Manipulations; +using Penumbra.Services; +using Penumbra.String.Classes; +using Penumbra.Util; + +namespace Penumbra.Mods; + + + +public class ModOptionEditor +{ + private readonly CommunicatorService _communicator; + private readonly FilenameService _filenames; + private readonly SaveService _saveService; + + public ModOptionEditor(CommunicatorService communicator, SaveService saveService, FilenameService filenames) + { + _communicator = communicator; + _saveService = saveService; + _filenames = filenames; + } + + /// 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)); + _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, uint defaultOption) + { + var group = mod._groups[groupIdx]; + if (group.DefaultSettings == defaultOption) + return; + + group.DefaultSettings = defaultOption; + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _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)); + var _ = group switch + { + SingleModGroup s => s.Name = newName, + MultiModGroup m => m.Name = newName, + _ => newName, + }; + + _saveService.ImmediateSave(new ModSaveGroup(mod, groupIdx)); + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1); + } + + /// Add a new mod, empty option group of the given type and name. + public void AddModGroup(Mod mod, GroupType type, string newName) + { + if (!VerifyFileName(mod, null, newName, true)) + return; + + var maxPriority = mod._groups.Count == 0 ? 0 : mod._groups.Max(o => o.Priority) + 1; + + mod._groups.Add(type == GroupType.Multi + ? new MultiModGroup + { + Name = newName, + Priority = maxPriority, + } + : new SingleModGroup + { + Name = newName, + Priority = maxPriority, + }); + _saveService.ImmediateSave(new ModSaveGroup(mod, mod._groups.Count - 1)); + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod._groups.Count - 1, -1, -1); + } + + /// Delete a given option group. Fires an event to prepare before actually deleting. + public void DeleteModGroup(Mod mod, int groupIdx) + { + var group = mod._groups[groupIdx]; + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1); + mod._groups.RemoveAt(groupIdx); + UpdateSubModPositions(mod, groupIdx); + _saveService.SaveAllOptionGroups(mod); + _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; + + UpdateSubModPositions(mod, Math.Min(groupIdxFrom, groupIdxTo)); + _saveService.SaveAllOptionGroups(mod); + _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; + + var _ = group switch + { + SingleModGroup s => s.Description = newDescription, + MultiModGroup m => m.Description = newDescription, + _ => newDescription, + }; + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _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 group = mod._groups[groupIdx]; + var option = group[optionIdx]; + if (option.Description == newDescription || option is not SubMod s) + return; + + s.Description = newDescription; + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _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, int newPriority) + { + var group = mod._groups[groupIdx]; + if (group.Priority == newPriority) + return; + + var _ = group switch + { + SingleModGroup s => s.Priority = newPriority, + MultiModGroup m => m.Priority = newPriority, + _ => newPriority, + }; + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _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, int newPriority) + { + switch (mod._groups[groupIdx]) + { + case SingleModGroup: + ChangeGroupPriority(mod, groupIdx, newPriority); + break; + case MultiModGroup m: + if (m.PrioritizedOptions[optionIdx].Priority == newPriority) + return; + + m.PrioritizedOptions[optionIdx] = (m.PrioritizedOptions[optionIdx].Mod, newPriority); + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _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) + { + switch (mod._groups[groupIdx]) + { + case SingleModGroup s: + if (s.OptionData[optionIdx].Name == newName) + return; + + s.OptionData[optionIdx].Name = newName; + break; + case MultiModGroup m: + var option = m.PrioritizedOptions[optionIdx].Mod; + if (option.Name == newName) + return; + + option.Name = newName; + break; + } + + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); + } + + /// Add a new empty option of the given name for the given group. + public void AddOption(Mod mod, int groupIdx, string newName) + { + var group = mod._groups[groupIdx]; + var subMod = new SubMod(mod) { Name = newName }; + subMod.SetPosition(groupIdx, group.Count); + switch (group) + { + case SingleModGroup s: + s.OptionData.Add(subMod); + break; + case MultiModGroup m: + m.PrioritizedOptions.Add((subMod, 0)); + break; + } + + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); + } + + /// Add an existing option to a given group with a given priority. + public void AddOption(Mod mod, int groupIdx, ISubMod option, int priority = 0) + { + if (option is not SubMod o) + return; + + var group = mod._groups[groupIdx]; + if (group.Type is GroupType.Multi && group.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; + } + + o.SetPosition(groupIdx, group.Count); + + switch (group) + { + case SingleModGroup s: + s.OptionData.Add(o); + break; + case MultiModGroup m: + m.PrioritizedOptions.Add((o, priority)); + break; + } + + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); + } + + /// Delete the given option from the given group. + public void DeleteOption(Mod mod, int groupIdx, int optionIdx) + { + 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.PrioritizedOptions.RemoveAt(optionIdx); + break; + } + + group.UpdatePositions(optionIdx); + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1); + } + + /// Move an option inside the given option group. + public void MoveOption(Mod mod, int groupIdx, int optionIdxFrom, int optionIdxTo) + { + var group = mod._groups[groupIdx]; + if (!group.MoveOption(optionIdxFrom, optionIdxTo)) + return; + + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, mod, groupIdx, optionIdxFrom, optionIdxTo); + } + + /// Set the meta manipulations for a given option. Replaces existing manipulations. + public void OptionSetManipulations(Mod mod, int groupIdx, int optionIdx, HashSet manipulations) + { + var subMod = GetSubMod(mod, groupIdx, optionIdx); + if (subMod.Manipulations.Count == manipulations.Count + && subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m))) + return; + + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); + subMod.ManipulationData = manipulations; + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1); + } + + /// Set the file redirections for a given option. Replaces existing redirections. + public void OptionSetFiles(Mod mod, int groupIdx, int optionIdx, Dictionary replacements) + { + var subMod = GetSubMod(mod, groupIdx, optionIdx); + if (subMod.FileData.SetEquals(replacements)) + return; + + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); + subMod.FileData = replacements; + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1); + } + + /// Add additional file redirections to a given option, keeping already existing ones. Only fires an event if anything is actually added. + public void OptionAddFiles(Mod mod, int groupIdx, int optionIdx, Dictionary additions) + { + var subMod = GetSubMod(mod, groupIdx, optionIdx); + var oldCount = subMod.FileData.Count; + subMod.FileData.AddFrom(additions); + if (oldCount != subMod.FileData.Count) + { + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, optionIdx, -1); + } + } + + /// Set the file swaps for a given option. Replaces existing swaps. + public void OptionSetFileSwaps(Mod mod, int groupIdx, int optionIdx, Dictionary swaps) + { + var subMod = GetSubMod(mod, groupIdx, optionIdx); + if (subMod.FileSwapData.SetEquals(swaps)) + return; + + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); + subMod.FileSwapData = swaps; + _saveService.QueueSave(new ModSaveGroup(mod, groupIdx)); + _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -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.ChatService.NotificationMessage( + $"Could not name option {newName} because option with same filename {path} already exists.", + "Warning", NotificationType.Warning); + + return false; + } + + /// Update the indices stored in options from a given group on. + private static void UpdateSubModPositions(Mod mod, int fromGroup) + { + foreach (var (group, groupIdx) in mod._groups.WithIndex().Skip(fromGroup)) + { + foreach (var (o, optionIdx) in group.OfType().WithIndex()) + o.SetPosition(groupIdx, optionIdx); + } + } + + /// Get the correct option for the given group and option index. + private static SubMod GetSubMod(Mod mod, int groupIdx, int optionIdx) + { + if (groupIdx == -1 && optionIdx == 0) + return mod._default; + + return mod._groups[groupIdx] switch + { + SingleModGroup s => s.OptionData[optionIdx], + MultiModGroup m => m.PrioritizedOptions[optionIdx].Mod, + _ => throw new InvalidOperationException(), + }; + } +} diff --git a/Penumbra/Mods/Mod.BasePath.cs b/Penumbra/Mods/Mod.BasePath.cs index fb71ade6..41da763b 100644 --- a/Penumbra/Mods/Mod.BasePath.cs +++ b/Penumbra/Mods/Mod.BasePath.cs @@ -101,7 +101,7 @@ public partial class Mod if( changes ) { - SaveAllGroups(); + Penumbra.SaveService.SaveAllOptionGroups(this); } } } \ No newline at end of file diff --git a/Penumbra/Mods/Mod.Creator.cs b/Penumbra/Mods/Mod.Creator.cs index f67dbf33..87ebd502 100644 --- a/Penumbra/Mods/Mod.Creator.cs +++ b/Penumbra/Mods/Mod.Creator.cs @@ -4,10 +4,8 @@ using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; -using Dalamud.Utility; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using OtterGui.Classes; using OtterGui.Filesystem; using Penumbra.Api.Enums; using Penumbra.Import.Structs; @@ -79,8 +77,8 @@ public partial class Mod Priority = priority, DefaultSettings = defaultSettings, }; - group.PrioritizedOptions.AddRange( subMods.OfType< SubMod >().Select( ( s, idx ) => ( s, idx ) ) ); - IModGroup.Save( group, baseFolder, index ); + group.PrioritizedOptions.AddRange( subMods.OfType< SubMod >().Select( ( s, idx ) => ( s, idx ) ) ); + Penumbra.SaveService.ImmediateSave(new ModSaveGroup(baseFolder, group, index)); break; } case GroupType.Single: @@ -93,7 +91,7 @@ public partial class Mod DefaultSettings = defaultSettings, }; group.OptionData.AddRange( subMods.OfType< SubMod >() ); - IModGroup.Save( group, baseFolder, index ); + Penumbra.SaveService.ImmediateSave(new ModSaveGroup(baseFolder, group, index)); break; } } diff --git a/Penumbra/Mods/Mod.Files.cs b/Penumbra/Mods/Mod.Files.cs index 6091ba1b..979a13fc 100644 --- a/Penumbra/Mods/Mod.Files.cs +++ b/Penumbra/Mods/Mod.Files.cs @@ -103,7 +103,7 @@ public partial class Mod var group = LoadModGroup( this, file, _groups.Count ); if( group != null && _groups.All( g => g.Name != group.Name ) ) { - changes = changes || group.FileName( ModPath, _groups.Count ) != file.FullName; + changes = changes || Penumbra.Filenames.OptionGroupFile(ModPath.FullName, Groups.Count, group.Name) != file.FullName; _groups.Add( group ); } else @@ -114,32 +114,7 @@ public partial class Mod if( changes ) { - SaveAllGroups(); - } - } - - // Delete all existing group files and save them anew. - // Used when indices change in complex ways. - internal void SaveAllGroups() - { - foreach( var file in GroupFiles ) - { - try - { - if( file.Exists ) - { - file.Delete(); - } - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not delete outdated group file {file}:\n{e}" ); - } - } - - foreach( var (group, index) in _groups.WithIndex() ) - { - IModGroup.Save( group, ModPath, index ); + Penumbra.SaveService.SaveAllOptionGroups(this); } } } \ No newline at end of file diff --git a/Penumbra/Mods/Mod.Meta.Migration.cs b/Penumbra/Mods/Mod.Meta.Migration.cs index 5a07fd29..9b09c294 100644 --- a/Penumbra/Mods/Mod.Meta.Migration.cs +++ b/Penumbra/Mods/Mod.Meta.Migration.cs @@ -85,8 +85,8 @@ public sealed partial class Mod mod._default.FileSwapData.Add(gamePath, swapPath); mod._default.IncorporateMetaChanges(mod.ModPath, true); - foreach (var (group, index) in mod.Groups.WithIndex()) - IModGroup.Save(group, mod.ModPath, index); + foreach (var (_, index) in mod.Groups.WithIndex()) + Penumbra.SaveService.ImmediateSave(new ModSaveGroup(mod, index)); // Delete meta files. foreach (var file in seenMetaFiles.Where(f => f.Exists)) diff --git a/Penumbra/Mods/Subclasses/IModGroup.cs b/Penumbra/Mods/Subclasses/IModGroup.cs index 0c1ebf2f..45db2b59 100644 --- a/Penumbra/Mods/Subclasses/IModGroup.cs +++ b/Penumbra/Mods/Subclasses/IModGroup.cs @@ -2,24 +2,25 @@ using System; using System.Collections.Generic; using System.IO; using Newtonsoft.Json; -using OtterGui.Filesystem; using Penumbra.Api.Enums; +using Penumbra.Services; +using Penumbra.Util; namespace Penumbra.Mods; -public interface IModGroup : IEnumerable< ISubMod > +public interface IModGroup : IEnumerable { public const int MaxMultiOptions = 32; - public string Name { get; } - public string Description { get; } - public GroupType Type { get; } - public int Priority { get; } - public uint DefaultSettings { get; set; } + public string Name { get; } + public string Description { get; } + public GroupType Type { get; } + public int Priority { get; } + public uint DefaultSettings { get; set; } - public int OptionPriority( Index optionIdx ); + public int OptionPriority(Index optionIdx); - public ISubMod this[ Index idx ] { get; } + public ISubMod this[Index idx] { get; } public int Count { get; } @@ -28,72 +29,76 @@ public interface IModGroup : IEnumerable< ISubMod > { GroupType.Single => Count > 1, GroupType.Multi => Count > 0, - _ => false, + _ => false, }; - public string FileName( DirectoryInfo basePath, int groupIdx ) - => Path.Combine( basePath.FullName, $"group_{groupIdx + 1:D3}_{Name.RemoveInvalidPathSymbols().ToLowerInvariant()}.json" ); + public IModGroup Convert(GroupType type); + public bool MoveOption(int optionIdxFrom, int optionIdxTo); + public void UpdatePositions(int from = 0); +} - public void DeleteFile( DirectoryInfo basePath, int groupIdx ) +public readonly struct ModSaveGroup : ISavable +{ + private readonly DirectoryInfo _basePath; + private readonly IModGroup? _group; + private readonly int _groupIdx; + private readonly ISubMod? _defaultMod; + + public ModSaveGroup(Mod mod, int groupIdx) { - var file = FileName( basePath, groupIdx ); - if( !File.Exists( file ) ) - { - return; - } - - try - { - File.Delete( file ); - Penumbra.Log.Debug( $"Deleted group file {file} for group {groupIdx + 1}: {Name}." ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not delete file {file}:\n{e}" ); - throw; - } + _basePath = mod.ModPath; + if (_groupIdx < 0) + _defaultMod = mod.Default; + else + _group = mod.Groups[groupIdx]; + _groupIdx = groupIdx; } - public static void SaveDelayed( IModGroup group, DirectoryInfo basePath, int groupIdx ) + public ModSaveGroup(DirectoryInfo basePath, IModGroup group, int groupIdx) { - Penumbra.Framework.RegisterDelayed( $"{nameof( SaveModGroup )}_{basePath.Name}_{group.Name}", - () => SaveModGroup( group, basePath, groupIdx ) ); + _basePath = basePath; + _group = group; + _groupIdx = groupIdx; } - public static void Save( IModGroup group, DirectoryInfo basePath, int groupIdx ) - => SaveModGroup( group, basePath, groupIdx ); - - private static void SaveModGroup( IModGroup group, DirectoryInfo basePath, int groupIdx ) + public ModSaveGroup(DirectoryInfo basePath, ISubMod @default) { - var file = group.FileName( basePath, groupIdx ); - using var s = File.Exists( file ) ? File.Open( file, FileMode.Truncate ) : File.Open( file, FileMode.CreateNew ); - using var writer = new StreamWriter( s ); - using var j = new JsonTextWriter( writer ) { Formatting = Formatting.Indented }; - var serializer = new JsonSerializer { Formatting = Formatting.Indented }; - j.WriteStartObject(); - j.WritePropertyName( nameof( group.Name ) ); - j.WriteValue( group.Name ); - j.WritePropertyName( nameof( group.Description ) ); - j.WriteValue( group.Description ); - j.WritePropertyName( nameof( group.Priority ) ); - j.WriteValue( group.Priority ); - j.WritePropertyName( nameof( Type ) ); - j.WriteValue( group.Type.ToString() ); - j.WritePropertyName( nameof( group.DefaultSettings ) ); - j.WriteValue( group.DefaultSettings ); - j.WritePropertyName( "Options" ); - j.WriteStartArray(); - for( var idx = 0; idx < group.Count; ++idx ) + _basePath = basePath; + _groupIdx = -1; + _defaultMod = @default; + } + + public string ToFilename(FilenameService fileNames) + => fileNames.OptionGroupFile(_basePath.FullName, _groupIdx, _group?.Name ?? string.Empty); + + public void Save(StreamWriter writer) + { + using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + var serializer = new JsonSerializer { Formatting = Formatting.Indented }; + if (_groupIdx >= 0) { - ISubMod.WriteSubMod( j, serializer, group[ idx ], basePath, group.Type == GroupType.Multi ? group.OptionPriority( idx ) : null ); + j.WriteStartObject(); + j.WritePropertyName(nameof(_group.Name)); + j.WriteValue(_group!.Name); + j.WritePropertyName(nameof(_group.Description)); + j.WriteValue(_group.Description); + j.WritePropertyName(nameof(_group.Priority)); + j.WriteValue(_group.Priority); + j.WritePropertyName(nameof(Type)); + j.WriteValue(_group.Type.ToString()); + j.WritePropertyName(nameof(_group.DefaultSettings)); + j.WriteValue(_group.DefaultSettings); + j.WritePropertyName("Options"); + j.WriteStartArray(); + for (var idx = 0; idx < _group.Count; ++idx) + ISubMod.WriteSubMod(j, serializer, _group[idx], _basePath, _group.Type == GroupType.Multi ? _group.OptionPriority(idx) : null); + + j.WriteEndArray(); + j.WriteEndObject(); + } + else + { + ISubMod.WriteSubMod(j, serializer, _defaultMod!, _basePath, null); } - - j.WriteEndArray(); - j.WriteEndObject(); - Penumbra.Log.Debug( $"Saved group file {file} for group {groupIdx + 1}: {group.Name}." ); } - - public IModGroup Convert( GroupType type ); - public bool MoveOption( int optionIdxFrom, int optionIdxTo ); - public void UpdatePositions( int from = 0 ); -} \ No newline at end of file +} diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 85a7c717..78931cf0 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -38,17 +38,18 @@ public class Penumbra : IDalamudPlugin public string Name => "Penumbra"; - public static Logger Log { get; private set; } = null!; - public static ChatService ChatService { get; private set; } = null!; - public static SaveService SaveService { get; private set; } = null!; - public static Configuration Config { get; private set; } = null!; + public static Logger Log { get; private set; } = null!; + public static ChatService ChatService { get; private set; } = null!; + public static FilenameService Filenames { get; private set; } = null!; + public static SaveService SaveService { get; private set; } = null!; + public static Configuration Config { get; private set; } = null!; public static ResidentResourceManager ResidentResources { get; private set; } = null!; public static CharacterUtility CharacterUtility { get; private set; } = null!; public static GameEventManager GameEvents { get; private set; } = null!; public static MetaFileManager MetaFileManager { get; private set; } = null!; - public static ModManager ModManager { get; private set; } = null!; - public static CollectionManager CollectionManager { get; private set; } = null!; + public static ModManager ModManager { get; private set; } = null!; + public static CollectionManager CollectionManager { get; private set; } = null!; public static TempCollectionManager TempCollections { get; private set; } = null!; public static TempModManager TempMods { get; private set; } = null!; public static ResourceLoader ResourceLoader { get; private set; } = null!; @@ -63,13 +64,13 @@ public class Penumbra : IDalamudPlugin public static PerformanceTracker Performance { get; private set; } = null!; - public readonly PathResolver PathResolver; - public readonly RedrawService RedrawService; - public readonly ModFileSystem ModFileSystem; - public HttpApi HttpApi = null!; - internal ConfigWindow? ConfigWindow { get; private set; } - private PenumbraWindowSystem? _windowSystem; - private bool _disposed; + public readonly PathResolver PathResolver; + public readonly RedrawService RedrawService; + public readonly ModFileSystem ModFileSystem; + public HttpApi HttpApi = null!; + internal ConfigWindow? ConfigWindow { get; private set; } + private PenumbraWindowSystem? _windowSystem; + private bool _disposed; private readonly PenumbraNew _tmp; @@ -80,29 +81,30 @@ public class Penumbra : IDalamudPlugin { _tmp = new PenumbraNew(this, pluginInterface); ChatService = _tmp.Services.GetRequiredService(); + Filenames = _tmp.Services.GetRequiredService(); SaveService = _tmp.Services.GetRequiredService(); Performance = _tmp.Services.GetRequiredService(); ValidityChecker = _tmp.Services.GetRequiredService(); _tmp.Services.GetRequiredService(); - Config = _tmp.Services.GetRequiredService(); - CharacterUtility = _tmp.Services.GetRequiredService(); - GameEvents = _tmp.Services.GetRequiredService(); - MetaFileManager = _tmp.Services.GetRequiredService(); - Framework = _tmp.Services.GetRequiredService(); - Actors = _tmp.Services.GetRequiredService().AwaitedService; - Identifier = _tmp.Services.GetRequiredService().AwaitedService; - GamePathParser = _tmp.Services.GetRequiredService(); - StainService = _tmp.Services.GetRequiredService(); - TempMods = _tmp.Services.GetRequiredService(); - ResidentResources = _tmp.Services.GetRequiredService(); + Config = _tmp.Services.GetRequiredService(); + CharacterUtility = _tmp.Services.GetRequiredService(); + GameEvents = _tmp.Services.GetRequiredService(); + MetaFileManager = _tmp.Services.GetRequiredService(); + Framework = _tmp.Services.GetRequiredService(); + Actors = _tmp.Services.GetRequiredService().AwaitedService; + Identifier = _tmp.Services.GetRequiredService().AwaitedService; + GamePathParser = _tmp.Services.GetRequiredService(); + StainService = _tmp.Services.GetRequiredService(); + TempMods = _tmp.Services.GetRequiredService(); + ResidentResources = _tmp.Services.GetRequiredService(); _tmp.Services.GetRequiredService(); - ModManager = _tmp.Services.GetRequiredService(); - CollectionManager = _tmp.Services.GetRequiredService(); - TempCollections = _tmp.Services.GetRequiredService(); - ModFileSystem = _tmp.Services.GetRequiredService(); - RedrawService = _tmp.Services.GetRequiredService(); + ModManager = _tmp.Services.GetRequiredService(); + CollectionManager = _tmp.Services.GetRequiredService(); + TempCollections = _tmp.Services.GetRequiredService(); + ModFileSystem = _tmp.Services.GetRequiredService(); + RedrawService = _tmp.Services.GetRequiredService(); _tmp.Services.GetRequiredService(); - ResourceLoader = _tmp.Services.GetRequiredService(); + ResourceLoader = _tmp.Services.GetRequiredService(); using (var t = _tmp.Services.GetRequiredService().Measure(StartTimeType.PathResolver)) { PathResolver = _tmp.Services.GetRequiredService(); @@ -112,7 +114,8 @@ public class Penumbra : IDalamudPlugin SetupApi(); ValidityChecker.LogExceptions(); - Log.Information($"Penumbra Version {ValidityChecker.Version}, Commit #{ValidityChecker.CommitHash} successfully Loaded from {pluginInterface.SourceRepository}."); + Log.Information( + $"Penumbra Version {ValidityChecker.Version}, Commit #{ValidityChecker.CommitHash} successfully Loaded from {pluginInterface.SourceRepository}."); OtterTex.NativeDll.Initialize(pluginInterface.AssemblyLocation.DirectoryName); Log.Information($"Loading native OtterTex assembly from {OtterTex.NativeDll.Directory}."); @@ -129,8 +132,8 @@ public class Penumbra : IDalamudPlugin private void SetupApi() { using var timer = _tmp.Services.GetRequiredService().Measure(StartTimeType.Api); - var api = _tmp.Services.GetRequiredService(); - HttpApi = _tmp.Services.GetRequiredService(); + var api = _tmp.Services.GetRequiredService(); + HttpApi = _tmp.Services.GetRequiredService(); _tmp.Services.GetRequiredService(); if (Config.EnableHttpApi) HttpApi.CreateWebServer(); diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index fcf2c386..9d976a9a 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -93,6 +93,7 @@ public class PenumbraNew // Add Mod Services services.AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton(); diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index f4381883..18c64eac 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -45,13 +45,22 @@ public class CommunicatorService : IDisposable /// public readonly EventWrapper CreatedCharacterBase = new(nameof(CreatedCharacterBase)); - /// + /// /// Parameter is the type of data change for the mod, which can be multiple flags. /// Parameter is the changed mod. /// Parameter is the old name of the mod in case of a name change, and null otherwise. /// public readonly EventWrapper ModDataChanged = new(nameof(ModDataChanged)); + /// + /// 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. + /// + public readonly EventWrapper ModOptionChanged = new(nameof(ModOptionChanged)); + public void Dispose() { CollectionChange.Dispose(); @@ -60,5 +69,6 @@ public class CommunicatorService : IDisposable CreatingCharacterBase.Dispose(); CreatedCharacterBase.Dispose(); ModDataChanged.Dispose(); + ModOptionChanged.Dispose(); } } diff --git a/Penumbra/Services/FilenameService.cs b/Penumbra/Services/FilenameService.cs index 26721257..d7060e05 100644 --- a/Penumbra/Services/FilenameService.cs +++ b/Penumbra/Services/FilenameService.cs @@ -71,4 +71,21 @@ public class FilenameService /// Obtain the path of the meta file given a mod directory. public string ModMetaPath(string modDirectory) => Path.Combine(modDirectory, "meta.json"); + + /// Obtain the path of the file describing a given option group by its index and the mod. If the index is < 0, return the path for the default mod file. + public string OptionGroupFile(Mod mod, int index) + => OptionGroupFile(mod.ModPath.FullName, index, index >= 0 ? mod.Groups[index].Name : string.Empty); + + /// Obtain the path of the file describing a given option group by its index, name and basepath. If the index is < 0, return the path for the default mod file. + public string OptionGroupFile(string basePath, int index, string name) + { + var fileName = index >= 0 + ? $"group_{index + 1:D3}_{name.RemoveInvalidPathSymbols().ToLowerInvariant()}.json" + : "default_mod.json"; + return Path.Combine(basePath, fileName); + } + + /// Enumerate all group files for a given mod. + public IEnumerable GetOptionGroupFiles(Mod mod) + => mod.ModPath.EnumerateFiles("group_*.json"); } diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 48333d6f..9a0af228 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -270,7 +270,7 @@ public class ItemSwapTab : IDisposable, ITab _modManager.DataEditor.CreateMeta(newDir, _newModName, _config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty); Mod.Creator.CreateDefaultFiles(newDir); _modManager.AddMod(newDir); - if (!_swapData.WriteMod(_modManager.Last(), + if (!_swapData.WriteMod(_modManager, _modManager.Last(), _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps)) _modManager.DeleteMod(_modManager.Count - 1); } @@ -296,16 +296,16 @@ public class ItemSwapTab : IDisposable, ITab { if (_selectedGroup == null) { - _modManager.AddModGroup(_mod, GroupType.Multi, _newGroupName); + _modManager.OptionEditor.AddModGroup(_mod, GroupType.Multi, _newGroupName); _selectedGroup = _mod.Groups.Last(); groupCreated = true; } - _modManager.AddOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _newOptionName); + _modManager.OptionEditor.AddOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _newOptionName); optionCreated = true; optionFolderName = Directory.CreateDirectory(optionFolderName.FullName); dirCreated = true; - if (!_swapData.WriteMod(_mod, _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps, + if (!_swapData.WriteMod(_modManager, _mod, _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps, optionFolderName, _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1)) throw new Exception("Failure writing files for mod swap."); @@ -317,11 +317,11 @@ public class ItemSwapTab : IDisposable, ITab try { if (optionCreated && _selectedGroup != null) - _modManager.DeleteOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1); + _modManager.OptionEditor.DeleteOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1); if (groupCreated) { - _modManager.DeleteModGroup(_mod, _mod.Groups.IndexOf(_selectedGroup!)); + _modManager.OptionEditor.DeleteModGroup(_mod, _mod.Groups.IndexOf(_selectedGroup!)); _selectedGroup = null; } diff --git a/Penumbra/UI/ModsTab/ModPanelEditTab.cs b/Penumbra/UI/ModsTab/ModPanelEditTab.cs index 3d0068d2..f8a82b75 100644 --- a/Penumbra/UI/ModsTab/ModPanelEditTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelEditTab.cs @@ -12,6 +12,7 @@ using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Api.Enums; using Penumbra.Mods; +using Penumbra.Services; using Penumbra.UI.AdvancedWindow; using Penumbra.Util; @@ -20,7 +21,8 @@ namespace Penumbra.UI.ModsTab; public class ModPanelEditTab : ITab { private readonly ChatService _chat; - private readonly ModManager _modManager; + private readonly FilenameService _filenames; + private readonly ModManager _modManager; private readonly ModFileSystem _fileSystem; private readonly ModFileSystemSelector _selector; private readonly ModEditWindow _editWindow; @@ -34,14 +36,15 @@ public class ModPanelEditTab : ITab private Mod _mod = null!; public ModPanelEditTab(ModManager modManager, ModFileSystemSelector selector, ModFileSystem fileSystem, ChatService chat, - ModEditWindow editWindow, ModEditor editor) + ModEditWindow editWindow, ModEditor editor, FilenameService filenames) { - _modManager = modManager; - _selector = selector; - _fileSystem = fileSystem; - _chat = chat; - _editWindow = editWindow; - _editor = editor; + _modManager = modManager; + _selector = selector; + _fileSystem = fileSystem; + _chat = chat; + _editWindow = editWindow; + _editor = editor; + _filenames = filenames; } public ReadOnlySpan Label @@ -129,7 +132,7 @@ public class ModPanelEditTab : ITab if (ImGui.Button("Update Bibo Material", buttonSize)) { _editor.LoadMod(_mod); - _editor.MdlMaterialEditor.ReplaceAllMaterials("bibo", "b"); + _editor.MdlMaterialEditor.ReplaceAllMaterials("bibo", "b"); _editor.MdlMaterialEditor.ReplaceAllMaterials("bibopube", "c"); _editor.MdlMaterialEditor.SaveAllModels(); _editWindow.UpdateModels(); @@ -189,7 +192,7 @@ public class ModPanelEditTab : ITab var reducedSize = new Vector2(UiHelpers.InputTextMinusButton3, 0); if (ImGui.Button("Edit Description", reducedSize)) - _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_mod, Input.Description)); + _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_filenames, _mod, Input.Description)); ImGui.SameLine(); var fileExists = File.Exists(_modManager.DataEditor.MetaFile(_mod)); @@ -235,13 +238,13 @@ public class ModPanelEditTab : ITab ImGui.SameLine(); - var nameValid = modManager.VerifyFileName(mod, null, _newGroupName, false); + var nameValid = ModOptionEditor.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.AddModGroup(mod, GroupType.Single, _newGroupName); + modManager.OptionEditor.AddModGroup(mod, GroupType.Single, _newGroupName); Reset(); } } @@ -249,7 +252,7 @@ public class ModPanelEditTab : ITab /// A text input for the new directory name and a button to apply the move. private static class MoveDirectory { - private static string? _currentModDirectory; + private static string? _currentModDirectory; private static ModManager.NewDirectoryState _state = ModManager.NewDirectoryState.Identical; public static void Reset() @@ -297,14 +300,16 @@ public class ModPanelEditTab : ITab /// Open a popup to edit a multi-line mod or option description. private static class DescriptionEdit { - private const string PopupName = "Edit Description"; - private static string _newDescription = string.Empty; - private static int _newDescriptionIdx = -1; - private static int _newDescriptionOptionIdx = -1; - private static Mod? _mod; + private const string PopupName = "Edit Description"; + private static string _newDescription = string.Empty; + private static int _newDescriptionIdx = -1; + private static int _newDescriptionOptionIdx = -1; + private static Mod? _mod; + private static FilenameService? _fileNames; - public static void OpenPopup(Mod mod, int groupIdx, int optionIdx = -1) + public static void OpenPopup(FilenameService filenames, Mod mod, int groupIdx, int optionIdx = -1) { + _fileNames = filenames; _newDescriptionIdx = groupIdx; _newDescriptionOptionIdx = optionIdx; _newDescription = groupIdx < 0 @@ -353,9 +358,10 @@ public class ModPanelEditTab : ITab break; case >= 0: if (_newDescriptionOptionIdx < 0) - modManager.ChangeGroupDescription(_mod, _newDescriptionIdx, _newDescription); + modManager.OptionEditor.ChangeGroupDescription(_mod, _newDescriptionIdx, _newDescription); else - modManager.ChangeOptionDescription(_mod, _newDescriptionIdx, _newDescriptionOptionIdx, _newDescription); + modManager.OptionEditor.ChangeOptionDescription(_mod, _newDescriptionIdx, _newDescriptionOptionIdx, + _newDescription); break; } @@ -384,18 +390,18 @@ public class ModPanelEditTab : ITab .Push(ImGuiStyleVar.ItemSpacing, _itemSpacing); if (Input.Text("##Name", groupIdx, Input.None, group.Name, out var newGroupName, 256, UiHelpers.InputTextWidth.X)) - _modManager.RenameModGroup(_mod, groupIdx, newGroupName); + _modManager.OptionEditor.RenameModGroup(_mod, groupIdx, 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.DeleteModGroup(_mod, groupIdx)); + _delayedActions.Enqueue(() => _modManager.OptionEditor.DeleteModGroup(_mod, groupIdx)); ImGui.SameLine(); if (Input.Priority("##Priority", groupIdx, Input.None, group.Priority, out var priority, 50 * UiHelpers.Scale)) - _modManager.ChangeGroupPriority(_mod, groupIdx, priority); + _modManager.OptionEditor.ChangeGroupPriority(_mod, groupIdx, priority); ImGuiUtil.HoverTooltip("Group Priority"); @@ -405,7 +411,7 @@ public class ModPanelEditTab : ITab 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.MoveModGroup(_mod, groupIdx, groupIdx - 1)); + _delayedActions.Enqueue(() => _modManager.OptionEditor.MoveModGroup(_mod, groupIdx, groupIdx - 1)); ImGui.SameLine(); tt = groupIdx == _mod.Groups.Count - 1 @@ -413,16 +419,16 @@ public class ModPanelEditTab : ITab : $"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.MoveModGroup(_mod, groupIdx, groupIdx + 1)); + _delayedActions.Enqueue(() => _modManager.OptionEditor.MoveModGroup(_mod, groupIdx, groupIdx + 1)); ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Edit.ToIconString(), UiHelpers.IconButtonSize, "Edit group description.", false, true)) - _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_mod, groupIdx)); + _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_filenames, _mod, groupIdx)); ImGui.SameLine(); - var fileName = group.FileName(_mod.ModPath, groupIdx); + var fileName = _filenames.OptionGroupFile(_mod, groupIdx); var fileExists = File.Exists(fileName); tt = fileExists ? $"Open the {group.Name} json file in the text editor of your choice." @@ -491,7 +497,7 @@ public class ModPanelEditTab : ITab if (group.Type == GroupType.Single) { if (ImGui.RadioButton("##default", group.DefaultSettings == optionIdx)) - panel._modManager.ChangeModGroupDefaultOption(panel._mod, groupIdx, (uint)optionIdx); + panel._modManager.OptionEditor.ChangeModGroupDefaultOption(panel._mod, groupIdx, (uint)optionIdx); ImGuiUtil.HoverTooltip($"Set {option.Name} as the default choice for this group."); } @@ -499,7 +505,7 @@ public class ModPanelEditTab : ITab { var isDefaultOption = ((group.DefaultSettings >> optionIdx) & 1) != 0; if (ImGui.Checkbox("##default", ref isDefaultOption)) - panel._modManager.ChangeModGroupDefaultOption(panel._mod, groupIdx, isDefaultOption + panel._modManager.OptionEditor.ChangeModGroupDefaultOption(panel._mod, groupIdx, isDefaultOption ? group.DefaultSettings | (1u << optionIdx) : group.DefaultSettings & ~(1u << optionIdx)); @@ -508,17 +514,17 @@ public class ModPanelEditTab : ITab ImGui.TableNextColumn(); if (Input.Text("##Name", groupIdx, optionIdx, option.Name, out var newOptionName, 256, -1)) - panel._modManager.RenameOption(panel._mod, groupIdx, optionIdx, newOptionName); + panel._modManager.OptionEditor.RenameOption(panel._mod, groupIdx, optionIdx, newOptionName); ImGui.TableNextColumn(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Edit.ToIconString(), UiHelpers.IconButtonSize, "Edit option description.", false, true)) - panel._delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(panel._mod, groupIdx, optionIdx)); + panel._delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(panel._filenames, panel._mod, groupIdx, optionIdx)); 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.DeleteOption(panel._mod, groupIdx, optionIdx)); + panel._delayedActions.Enqueue(() => panel._modManager.OptionEditor.DeleteOption(panel._mod, groupIdx, optionIdx)); ImGui.TableNextColumn(); if (group.Type != GroupType.Multi) @@ -526,7 +532,7 @@ public class ModPanelEditTab : ITab if (Input.Priority("##Priority", groupIdx, optionIdx, group.OptionPriority(optionIdx), out var priority, 50 * UiHelpers.Scale)) - panel._modManager.ChangeOptionPriority(panel._mod, groupIdx, optionIdx, priority); + panel._modManager.OptionEditor.ChangeOptionPriority(panel._mod, groupIdx, optionIdx, priority); ImGuiUtil.HoverTooltip("Option priority."); } @@ -560,7 +566,7 @@ public class ModPanelEditTab : ITab tt, !(canAddGroup && validName), true)) return; - panel._modManager.AddOption(mod, groupIdx, _newOptionName); + panel._modManager.OptionEditor.AddOption(mod, groupIdx, _newOptionName); _newOptionName = string.Empty; } @@ -591,7 +597,7 @@ public class ModPanelEditTab : ITab if (_dragDropGroupIdx == groupIdx) { var sourceOption = _dragDropOptionIdx; - panel._delayedActions.Enqueue(() => panel._modManager.MoveOption(panel._mod, groupIdx, sourceOption, optionIdx)); + panel._delayedActions.Enqueue(() => panel._modManager.OptionEditor.MoveOption(panel._mod, groupIdx, sourceOption, optionIdx)); } else { @@ -604,9 +610,9 @@ public class ModPanelEditTab : ITab var priority = sourceGroup.OptionPriority(_dragDropOptionIdx); panel._delayedActions.Enqueue(() => { - panel._modManager.DeleteOption(panel._mod, sourceGroupIdx, sourceOption); - panel._modManager.AddOption(panel._mod, groupIdx, option, priority); - panel._modManager.MoveOption(panel._mod, groupIdx, currentCount, optionIdx); + panel._modManager.OptionEditor.DeleteOption(panel._mod, sourceGroupIdx, sourceOption); + panel._modManager.OptionEditor.AddOption(panel._mod, groupIdx, option, priority); + panel._modManager.OptionEditor.MoveOption(panel._mod, groupIdx, currentCount, optionIdx); }); } } @@ -633,12 +639,12 @@ public class ModPanelEditTab : ITab return; if (ImGui.Selectable(GroupTypeName(GroupType.Single), group.Type == GroupType.Single)) - _modManager.ChangeModGroupType(_mod, groupIdx, GroupType.Single); + _modManager.OptionEditor.ChangeModGroupType(_mod, groupIdx, GroupType.Single); var canSwitchToMulti = group.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.ChangeModGroupType(_mod, groupIdx, GroupType.Multi); + _modManager.OptionEditor.ChangeModGroupType(_mod, groupIdx, GroupType.Multi); style.Pop(); if (!canSwitchToMulti) diff --git a/Penumbra/Util/SaveService.cs b/Penumbra/Util/SaveService.cs index 221ead81..8b256504 100644 --- a/Penumbra/Util/SaveService.cs +++ b/Penumbra/Util/SaveService.cs @@ -3,6 +3,7 @@ using System.IO; using System.Text; using OtterGui.Classes; using OtterGui.Log; +using Penumbra.Mods; using Penumbra.Services; namespace Penumbra.Util; @@ -94,4 +95,24 @@ public class SaveService _log.Error($"Could not delete {value.GetType().Name} {value.LogName(name)}:\n{ex}"); } } + + /// Immediately delete all existing option group files for a mod and save them anew. + public void SaveAllOptionGroups(Mod mod) + { + foreach (var file in _fileNames.GetOptionGroupFiles(mod)) + { + try + { + if (file.Exists) + file.Delete(); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not delete outdated group file {file}:\n{e}"); + } + } + + for (var i = 0; i < mod.Groups.Count; ++i) + ImmediateSave(new ModSaveGroup(mod, i)); + } }