using OtterGui; using OtterGui.Classes; using Penumbra.Mods.Groups; using Penumbra.Mods.Settings; using Penumbra.Mods.SubMods; using Penumbra.Services; namespace Penumbra.Mods.Manager.OptionEditor; public abstract class ModOptionEditor( CommunicatorService communicator, SaveService saveService, Configuration config) where TGroup : class, IModGroup where TOption : class, IModOption { 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 TGroup? AddModGroup(Mod mod, string newName, SaveType saveType = SaveType.ImmediateSync) { if (!ModGroupEditor.VerifyFileName(mod, null, newName, true)) return null; 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(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 (TGroup, int, bool) FindOrAddModGroup(Mod mod, string newName, SaveType saveType = SaveType.ImmediateSync) { var idx = mod.Groups.IndexOf(g => g.Name == newName); if (idx >= 0) { 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); } 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 (group, idx, true); } /// Add a new empty option of the given name for the given group. public TOption? AddOption(TGroup group, string newName, SaveType saveType = SaveType.Queue) { if (group.AddOption(newName) is not TOption option) return null; 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 (TOption, int, bool) FindOrAddOption(TGroup group, string newName, SaveType saveType = SaveType.Queue) { var idx = group.Options.IndexOf(o => o.Name == newName); if (idx >= 0) { 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); } if (AddOption(group, newName, saveType) is not { } option) throw new Exception($"Could not create new option with name {newName} in {group.Name}."); return (option, idx, true); } /// Add an existing option to a given group. public TOption? AddOption(TGroup group, IModOption option) { if (CloneOption(group, option) is not { } clonedOption) return null; 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(TOption option) { 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(TOption option, int optionIdxTo) { var idx = option.GetIndex(); var group = (TGroup)option.Group; if (!MoveOption(group, idx, optionIdxTo)) return; SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport)); Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, group.Mod, group, option, null, idx); } 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 { /// /// Give information for each type of change. /// If requiresSaving, collections need to be re-saved after this change. /// If requiresReloading, caches need to be manipulated after this change. /// If wasPrepared, caches have already removed the mod beforehand, then need add it again when this event is fired. /// Otherwise, caches need to reload the mod itself. /// public static void HandlingInfo(this ModOptionChangeType type, out bool requiresSaving, out bool requiresReloading, out bool wasPrepared) { (requiresSaving, requiresReloading, wasPrepared) = type switch { ModOptionChangeType.GroupRenamed => (true, false, false), ModOptionChangeType.GroupAdded => (true, false, false), ModOptionChangeType.GroupDeleted => (true, true, false), ModOptionChangeType.GroupMoved => (true, false, false), ModOptionChangeType.GroupTypeChanged => (true, true, true), ModOptionChangeType.PriorityChanged => (true, true, true), ModOptionChangeType.OptionAdded => (true, true, true), ModOptionChangeType.OptionDeleted => (true, true, false), ModOptionChangeType.OptionMoved => (true, false, false), ModOptionChangeType.OptionFilesChanged => (false, true, false), ModOptionChangeType.OptionFilesAdded => (false, true, true), ModOptionChangeType.OptionSwapsChanged => (false, true, false), ModOptionChangeType.OptionMetaChanged => (false, true, false), ModOptionChangeType.DisplayChange => (false, false, false), ModOptionChangeType.DefaultOptionChanged => (true, false, false), _ => (false, false, false), }; } }