Now that was a lot of work.

This commit is contained in:
Ottermandias 2024-04-26 18:43:45 +02:00
parent 297be487b5
commit 1e5ed1c414
44 changed files with 1182 additions and 766 deletions

@ -1 +1 @@
Subproject commit 590629df33f9ad92baddd1d65ec8c986f18d608a Subproject commit 69d106b457eb0f73d4b4caf1234da5631fd6fbf0

View file

@ -11,6 +11,7 @@ using Penumbra.Mods.Editor;
using Penumbra.Mods.Groups; using Penumbra.Mods.Groups;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.Mods.Settings; using Penumbra.Mods.Settings;
using Penumbra.Mods.SubMods;
using Penumbra.Services; using Penumbra.Services;
namespace Penumbra.Api.Api; 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) 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); => 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) switch (type)
{ {

View file

@ -7,8 +7,10 @@ using Penumbra.Communication;
using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.ResourceLoading;
using Penumbra.Meta; using Penumbra.Meta;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Mods.Groups;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.Mods.Settings; using Penumbra.Mods.Settings;
using Penumbra.Mods.SubMods;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.String.Classes; using Penumbra.String.Classes;
@ -257,7 +259,7 @@ public class CollectionCacheManager : IDisposable
} }
/// <summary> Prepare Changes by removing mods from caches with collections or add or reload mods. </summary> /// <summary> Prepare Changes by removing mods from caches with collections or add or reload mods. </summary>
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) if (type is ModOptionChangeType.PrepareChange)
{ {

View file

@ -4,8 +4,10 @@ using OtterGui.Classes;
using Penumbra.Communication; using Penumbra.Communication;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Mods.Editor; using Penumbra.Mods.Editor;
using Penumbra.Mods.Groups;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.Mods.Settings; using Penumbra.Mods.Settings;
using Penumbra.Mods.SubMods;
using Penumbra.Services; using Penumbra.Services;
namespace Penumbra.Collections.Manager; namespace Penumbra.Collections.Manager;
@ -290,7 +292,7 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
} }
/// <summary> Save all collections where the mod has settings and the change requires saving. </summary> /// <summary> Save all collections where the mod has settings and the change requires saving. </summary>
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 _); type.HandlingInfo(out var requiresSaving, out _, out _);
if (!requiresSaving) if (!requiresSaving)
@ -298,7 +300,7 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
foreach (var collection in this) 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)); _saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
} }
} }

View file

@ -1,8 +1,10 @@
using OtterGui.Classes; using OtterGui.Classes;
using Penumbra.Api;
using Penumbra.Api.Api; using Penumbra.Api.Api;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Mods.Groups;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.Mods.SubMods;
using static Penumbra.Communication.ModOptionChanged;
namespace Penumbra.Communication; namespace Penumbra.Communication;
@ -11,22 +13,23 @@ namespace Penumbra.Communication;
/// <list type="number"> /// <list type="number">
/// <item>Parameter is the type option change. </item> /// <item>Parameter is the type option change. </item>
/// <item>Parameter is the changed mod. </item> /// <item>Parameter is the changed mod. </item>
/// <item>Parameter is the index of the changed group inside the mod. </item> /// <item>Parameter is the changed group inside the mod. </item>
/// <item>Parameter is the index of the changed option inside the group or -1 if it does not concern a specific option. </item> /// <item>Parameter is the changed option inside the group or null if it does not concern a specific option. </item>
/// <item>Parameter is the index of the group an option was moved to. </item> /// <item>Parameter is the changed data container inside the group or null if it does not concern a specific data container. </item>
/// <item>Parameter is the index of the group or option moved or deleted from. </item>
/// </list> </summary> /// </list> </summary>
public sealed class ModOptionChanged() public sealed class ModOptionChanged()
: EventWrapper<ModOptionChangeType, Mod, int, int, int, ModOptionChanged.Priority>(nameof(ModOptionChanged)) : EventWrapper<ModOptionChangeType, Mod, IModGroup?, IModOption?, IModDataContainer?, int, Priority>(nameof(ModOptionChanged))
{ {
public enum Priority public enum Priority
{ {
/// <seealso cref="PenumbraApi.OnModOptionEdited"/> /// <seealso cref="ModSettingsApi.OnModOptionEdited"/>
Api = int.MinValue, Api = int.MinValue,
/// <seealso cref="Collections.Cache.CollectionCacheManager.OnModOptionChange"/> /// <seealso cref="Collections.Cache.CollectionCacheManager.OnModOptionChange"/>
CollectionCacheManager = -100, CollectionCacheManager = -100,
/// <seealso cref="Mods.Manager.ModCacheManager.OnModOptionChange"/> /// <seealso cref="ModCacheManager.OnModOptionChange"/>
ModCacheManager = 0, ModCacheManager = 0,
/// <seealso cref="UI.AdvancedWindow.ItemSwapTab.OnModOptionChange"/> /// <seealso cref="UI.AdvancedWindow.ItemSwapTab.OnModOptionChange"/>

View file

@ -205,7 +205,7 @@ public partial class TexToolsImporter
{ {
var option = group.OptionList[idx]; var option = group.OptionList[idx];
_currentOptionName = option.Name; _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) if (option.IsChecked)
defaultSettings = Setting.Single(idx); defaultSettings = Setting.Single(idx);
} }

View file

@ -60,7 +60,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co
private void HandleDuplicate(Mod mod, FullPath duplicate, FullPath remaining, bool useModManager) private void HandleDuplicate(Mod mod, FullPath duplicate, FullPath remaining, bool useModManager)
{ {
ModEditor.ApplyToAllOptions(mod, HandleSubMod); ModEditor.ApplyToAllContainers(mod, HandleSubMod);
try try
{ {
@ -73,7 +73,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co
return; return;
void HandleSubMod(IModDataContainer subMod, int groupIdx, int optionIdx) void HandleSubMod(IModDataContainer subMod)
{ {
var changes = false; var changes = false;
var dict = subMod.Files.ToDictionary(kvp => kvp.Key, var dict = subMod.Files.ToDictionary(kvp => kvp.Key,
@ -82,14 +82,9 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co
return; return;
if (useModManager) if (useModManager)
{ modManager.OptionEditor.SetFiles(subMod, dict, SaveType.ImmediateSync);
modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, dict, SaveType.ImmediateSync);
}
else else
{ saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, subMod, config.ReplaceNonAsciiOnImport));
subMod.Files = dict;
saveService.ImmediateSaveSync(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
}
} }
} }

View file

@ -105,23 +105,11 @@ public class ModEditor(
=> Clear(); => Clear();
/// <summary> Apply a option action to all available option in a mod, including the default option. </summary> /// <summary> Apply a option action to all available option in a mod, including the default option. </summary>
public static void ApplyToAllOptions(Mod mod, Action<IModDataContainer, int, int> action) public static void ApplyToAllContainers(Mod mod, Action<IModDataContainer> action)
{ {
action(mod.Default, -1, 0); action(mod.Default);
foreach (var (group, groupIdx) in mod.Groups.WithIndex()) foreach (var container in mod.Groups.SelectMany(g => g.DataContainers))
{ action(container);
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;
}
}
} }
// Does not delete the base directory itself even if it is completely empty at the end. // Does not delete the base directory itself even if it is completely empty at the end.

View file

@ -24,8 +24,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu
num += dict.TryAdd(path.Item2, file.File) ? 0 : 1; num += dict.TryAdd(path.Item2, file.File) ? 0 : 1;
} }
var (groupIdx, dataIdx) = option.GetDataIndices(); modManager.OptionEditor.SetFiles(option, dict);
modManager.OptionEditor.OptionSetFiles(mod, groupIdx, dataIdx, dict);
files.UpdatePaths(mod, option); files.UpdatePaths(mod, option);
Changes = false; Changes = false;
return num; return num;
@ -40,15 +39,15 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu
/// <summary> Remove all path redirections where the pointed-to file does not exist. </summary> /// <summary> Remove all path redirections where the pointed-to file does not exist. </summary>
public void RemoveMissingPaths(Mod mod, IModDataContainer option) 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)) var newDict = subMod.Files.Where(kvp => CheckAgainstMissing(mod, subMod, kvp.Value, kvp.Key, subMod == option))
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); .ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
if (newDict.Count != subMod.Files.Count) 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(); files.ClearMissingFiles();
} }

View file

@ -16,7 +16,7 @@ public class ModMerger : IDisposable
{ {
private readonly Configuration _config; private readonly Configuration _config;
private readonly CommunicatorService _communicator; private readonly CommunicatorService _communicator;
private readonly ModOptionEditor _editor; private readonly ModGroupEditor _editor;
private readonly ModFileSystemSelector _selector; private readonly ModFileSystemSelector _selector;
private readonly DuplicateManager _duplicates; private readonly DuplicateManager _duplicates;
private readonly ModManager _mods; private readonly ModManager _mods;
@ -32,14 +32,14 @@ public class ModMerger : IDisposable
private readonly Dictionary<string, string> _fileToFile = []; private readonly Dictionary<string, string> _fileToFile = [];
private readonly HashSet<string> _createdDirectories = []; private readonly HashSet<string> _createdDirectories = [];
private readonly HashSet<int> _createdGroups = []; private readonly HashSet<int> _createdGroups = [];
private readonly HashSet<IModDataOption> _createdOptions = []; private readonly HashSet<IModOption> _createdOptions = [];
public readonly HashSet<IModDataContainer> SelectedOptions = []; public readonly HashSet<IModDataContainer> SelectedOptions = [];
public readonly IReadOnlyList<string> Warnings = []; public readonly IReadOnlyList<string> Warnings = [];
public Exception? Error { get; private set; } 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) CommunicatorService communicator, ModCreator creator, Configuration config)
{ {
_editor = editor; _editor = editor;
@ -100,22 +100,23 @@ public class ModMerger : IDisposable
var (group, groupIdx, groupCreated) = _editor.FindOrAddModGroup(MergeToMod!, originalGroup.Type, originalGroup.Name); var (group, groupIdx, groupCreated) = _editor.FindOrAddModGroup(MergeToMod!, originalGroup.Type, originalGroup.Name);
if (groupCreated) if (groupCreated)
_createdGroups.Add(groupIdx); _createdGroups.Add(groupIdx);
if (group.Type != originalGroup.Type) if (group == null)
((List<string>)Warnings).Add( throw new Exception(
$"The merged group {group.Name} already existed, but has a different type {group.Type} than the original group of type {originalGroup.Type}."); $"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) 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) if (optionCreated)
{ {
_createdOptions.Add((IModDataOption)option); _createdOptions.Add(option!);
MergeIntoOption([originalOption], (IModDataOption)option, false); // #TODO DataContainer <> Option.
MergeIntoOption([originalOption], (IModDataContainer)option!, false);
} }
else else
{ {
throw new Exception( 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); var (group, groupIdx, groupCreated) = _editor.FindOrAddModGroup(MergeToMod!, GroupType.Multi, groupName, SaveType.None);
if (groupCreated) if (groupCreated)
_createdGroups.Add(groupIdx); _createdGroups.Add(groupIdx);
var (option, _, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, optionName, SaveType.None); var (option, _, optionCreated) = _editor.FindOrAddOption(group!, optionName, SaveType.None);
if (optionCreated) if (optionCreated)
_createdOptions.Add((IModDataOption)option); _createdOptions.Add(option!);
var dir = ModCreator.NewOptionDirectory(MergeToMod!.ModPath, groupName, _config.ReplaceNonAsciiOnImport); var dir = ModCreator.NewOptionDirectory(MergeToMod!.ModPath, groupName, _config.ReplaceNonAsciiOnImport);
if (!dir.Exists) if (!dir.Exists)
_createdDirectories.Add(dir.FullName); _createdDirectories.Add(dir.FullName);
@ -148,7 +149,8 @@ public class ModMerger : IDisposable
if (!dir.Exists) if (!dir.Exists)
_createdDirectories.Add(dir.FullName); _createdDirectories.Add(dir.FullName);
CopyFiles(dir); CopyFiles(dir);
MergeIntoOption(MergeFromMod!.AllDataContainers.Reverse(), (IModDataOption)option, true); // #TODO DataContainer <> Option.
MergeIntoOption(MergeFromMod!.AllDataContainers.Reverse(), (IModDataContainer)option!, true);
} }
private void MergeIntoOption(IEnumerable<IModDataContainer> mergeOptions, IModDataContainer option, bool fromFileToFile) private void MergeIntoOption(IEnumerable<IModDataContainer> mergeOptions, IModDataContainer option, bool fromFileToFile)
@ -184,10 +186,9 @@ public class ModMerger : IDisposable
} }
} }
var (groupIdx, dataIdx) = option.GetDataIndices(); _editor.SetFiles(option, redirections, SaveType.None);
_editor.OptionSetFiles(MergeToMod!, groupIdx, dataIdx, redirections, SaveType.None); _editor.SetFileSwaps(option, swaps, SaveType.None);
_editor.OptionSetFileSwaps(MergeToMod!, groupIdx, dataIdx, swaps, SaveType.None); _editor.SetManipulations(option, manips, SaveType.ImmediateSync);
_editor.OptionSetManipulations(MergeToMod!, groupIdx, dataIdx, manips, SaveType.ImmediateSync);
return; return;
bool GetFullPath(FullPath input, out FullPath ret) bool GetFullPath(FullPath input, out FullPath ret)
@ -261,30 +262,31 @@ public class ModMerger : IDisposable
if (mods.Count == 1) if (mods.Count == 1)
{ {
var files = CopySubModFiles(mods[0], dir); var files = CopySubModFiles(mods[0], dir);
_editor.OptionSetFiles(result, -1, 0, files); _editor.SetFiles(result.Default, files);
_editor.OptionSetFileSwaps(result, -1, 0, mods[0].FileSwaps); _editor.SetFileSwaps(result.Default, mods[0].FileSwaps);
_editor.OptionSetManipulations(result, -1, 0, mods[0].Manipulations); _editor.SetManipulations(result.Default, mods[0].Manipulations);
} }
else else
{ {
foreach (var originalOption in mods) foreach (var originalOption in mods)
{ {
if (originalOption.Group is not {} originalGroup) if (originalOption.Group is not { } originalGroup)
{ {
var files = CopySubModFiles(mods[0], dir); var files = CopySubModFiles(mods[0], dir);
_editor.OptionSetFiles(result, -1, 0, files); _editor.SetFiles(result.Default, files);
_editor.OptionSetFileSwaps(result, -1, 0, mods[0].FileSwaps); _editor.SetFileSwaps(result.Default, mods[0].FileSwaps);
_editor.OptionSetManipulations(result, -1, 0, mods[0].Manipulations); _editor.SetManipulations(result.Default, mods[0].Manipulations);
} }
else else
{ {
var (group, groupIdx, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name); // TODO DataContainer <> Option.
var (option, optionIdx, _) = _editor.FindOrAddOption(result, groupIdx, originalOption.GetName()); var (group, _, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name);
var folder = Path.Combine(dir.FullName, group.Name, option.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)); var files = CopySubModFiles(originalOption, new DirectoryInfo(folder));
_editor.OptionSetFiles(result, groupIdx, optionIdx, files); _editor.SetFiles((IModDataContainer)option, files);
_editor.OptionSetFileSwaps(result, groupIdx, optionIdx, originalOption.FileSwaps); _editor.SetFileSwaps((IModDataContainer)option, originalOption.FileSwaps);
_editor.OptionSetManipulations(result, groupIdx, optionIdx, originalOption.Manipulations); _editor.SetManipulations((IModDataContainer)option, originalOption.Manipulations);
} }
} }
} }
@ -339,16 +341,15 @@ public class ModMerger : IDisposable
{ {
foreach (var option in _createdOptions) foreach (var option in _createdOptions)
{ {
var (groupIdx, optionIdx) = option.GetOptionIndices(); _editor.DeleteOption(option);
_editor.DeleteOption(MergeToMod!, groupIdx, optionIdx);
Penumbra.Log.Verbose($"[Merger] Removed option {option.FullName}."); Penumbra.Log.Verbose($"[Merger] Removed option {option.FullName}.");
} }
foreach (var group in _createdGroups) foreach (var group in _createdGroups)
{ {
var groupName = MergeToMod!.Groups[group]; var groupName = MergeToMod!.Groups[group];
_editor.DeleteModGroup(MergeToMod!, group); _editor.DeleteModGroup(groupName);
Penumbra.Log.Verbose($"[Merger] Removed option group {groupName}."); Penumbra.Log.Verbose($"[Merger] Removed option group {groupName.Name}.");
} }
foreach (var dir in _createdDirectories) foreach (var dir in _createdDirectories)

View file

@ -145,12 +145,12 @@ public class ModMetaEditor(ModManager modManager)
Split(currentOption.Manipulations); Split(currentOption.Manipulations);
} }
public void Apply(Mod mod, int groupIdx, int optionIdx) public void Apply(IModDataContainer container)
{ {
if (!Changes) if (!Changes)
return; return;
modManager.OptionEditor.OptionSetManipulations(mod, groupIdx, optionIdx, Recombine().ToHashSet()); modManager.OptionEditor.SetManipulations(container, Recombine().ToHashSet());
Changes = false; Changes = false;
} }

View file

@ -283,12 +283,12 @@ public class ModNormalizer(ModManager _modManager, Configuration _config)
switch (group) switch (group)
{ {
case SingleModGroup single: case SingleModGroup single:
foreach (var (_, optionIdx) in single.OptionData.WithIndex()) foreach (var (option, optionIdx) in single.OptionData.WithIndex())
_modManager.OptionEditor.OptionSetFiles(Mod, groupIdx, optionIdx, _redirections[groupIdx + 1][optionIdx]); _modManager.OptionEditor.SetFiles(option, _redirections[groupIdx + 1][optionIdx]);
break; break;
case MultiModGroup multi: case MultiModGroup multi:
foreach (var (_, optionIdx) in multi.OptionData.WithIndex()) foreach (var (option, optionIdx) in multi.OptionData.WithIndex())
_modManager.OptionEditor.OptionSetFiles(Mod, groupIdx, optionIdx, _redirections[groupIdx + 1][optionIdx]); _modManager.OptionEditor.SetFiles(option, _redirections[groupIdx + 1][optionIdx]);
break; break;
} }
} }

View file

@ -17,12 +17,12 @@ public class ModSwapEditor(ModManager modManager)
Changes = false; Changes = false;
} }
public void Apply(Mod mod, int groupIdx, int optionIdx) public void Apply(IModDataContainer container)
{ {
if (!Changes) if (!Changes)
return; return;
modManager.OptionEditor.OptionSetFileSwaps(mod, groupIdx, optionIdx, _swaps); modManager.OptionEditor.SetFileSwaps(container, _swaps);
Changes = false; Changes = false;
} }

View file

@ -9,7 +9,7 @@ namespace Penumbra.Mods.Groups;
public interface ITexToolsGroup public interface ITexToolsGroup
{ {
public IReadOnlyList<IModDataOption> OptionData { get; } public IReadOnlyList<OptionSubMod> OptionData { get; }
} }
public interface IModGroup public interface IModGroup
@ -17,22 +17,19 @@ public interface IModGroup
public const int MaxMultiOptions = 63; public const int MaxMultiOptions = 63;
public Mod Mod { get; } public Mod Mod { get; }
public string Name { get; } public string Name { get; set; }
public string Description { get; set; } public string Description { get; set; }
public GroupType Type { get; } public GroupType Type { get; }
public ModPriority Priority { get; set; } public ModPriority Priority { get; set; }
public Setting DefaultSettings { get; set; } public Setting DefaultSettings { get; set; }
public FullPath? FindBestMatch(Utf8GamePath gamePath); public FullPath? FindBestMatch(Utf8GamePath gamePath);
public int AddOption(Mod mod, string name, string description = ""); public IModOption? AddOption(string name, string description = "");
public IReadOnlyList<IModOption> Options { get; } public IReadOnlyList<IModOption> Options { get; }
public IReadOnlyList<IModDataContainer> DataContainers { get; } public IReadOnlyList<IModDataContainer> DataContainers { get; }
public bool IsOption { get; } public bool IsOption { get; }
public IModGroup Convert(GroupType type);
public bool MoveOption(int optionIdxFrom, int optionIdxTo);
public int GetIndex(); public int GetIndex();
public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations); public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations);

View file

@ -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<ImcSubMod> OptionData = [];
public IReadOnlyList<IModOption> Options
=> OptionData;
public IReadOnlyList<IModDataContainer> 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<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> 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);
}

View file

@ -5,6 +5,7 @@ namespace Penumbra.Mods.Groups;
public static class ModGroup public static class ModGroup
{ {
/// <summary> Create a new mod group based on the given type. </summary>
public static IModGroup Create(Mod mod, GroupType type, string name) 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; 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, Name = name,
Priority = maxPriority, Priority = maxPriority,
}, },
GroupType.Imc => new ImcModGroup(mod)
{
Name = name,
Priority = maxPriority,
},
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null), _ => throw new ArgumentOutOfRangeException(nameof(type), type, null),
}; };
} }
@ -39,4 +45,13 @@ public static class ModGroup
return (redirectionCount, swapCount, manipCount); 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;
}
} }

View file

@ -12,25 +12,21 @@ public readonly struct ModSaveGroup : ISavable
private readonly DefaultSubMod? _defaultMod; private readonly DefaultSubMod? _defaultMod;
private readonly bool _onlyAscii; private readonly bool _onlyAscii;
public ModSaveGroup(Mod mod, int groupIdx, bool onlyAscii) private ModSaveGroup(DirectoryInfo basePath, IModGroup group, int groupIndex, 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)
{ {
_basePath = basePath; _basePath = basePath;
_group = group; _group = group;
_groupIdx = groupIdx; _groupIdx = groupIndex;
_onlyAscii = onlyAscii; _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) public ModSaveGroup(DirectoryInfo basePath, DefaultSubMod @default, bool onlyAscii)
{ {
_basePath = basePath; _basePath = basePath;
@ -39,6 +35,33 @@ public readonly struct ModSaveGroup : ISavable
_onlyAscii = onlyAscii; _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) public string ToFilename(FilenameService fileNames)
=> fileNames.OptionGroupFile(_basePath.FullName, _groupIdx, _group?.Name ?? string.Empty, _onlyAscii); => fileNames.OptionGroupFile(_basePath.FullName, _groupIdx, _group?.Name ?? string.Empty, _onlyAscii);
@ -59,7 +82,7 @@ public readonly struct ModSaveGroup : ISavable
{ {
jWriter.WriteStartObject(); jWriter.WriteStartObject();
jWriter.WritePropertyName(nameof(group.Name)); jWriter.WritePropertyName(nameof(group.Name));
jWriter.WriteValue(group!.Name); jWriter.WriteValue(group.Name);
jWriter.WritePropertyName(nameof(group.Description)); jWriter.WritePropertyName(nameof(group.Description));
jWriter.WriteValue(group.Description); jWriter.WriteValue(group.Description);
jWriter.WritePropertyName(nameof(group.Priority)); jWriter.WritePropertyName(nameof(group.Priority));

View file

@ -3,7 +3,6 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using OtterGui; using OtterGui;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Filesystem;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Settings; using Penumbra.Mods.Settings;
@ -18,7 +17,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup
public GroupType Type public GroupType Type
=> GroupType.Multi; => GroupType.Multi;
public Mod Mod { get; set; } = mod; public Mod Mod { get; } = mod;
public string Name { get; set; } = "Group"; public string Name { get; set; } = "Group";
public string Description { get; set; } = "A non-exclusive group of settings."; public string Description { get; set; } = "A non-exclusive group of settings.";
public ModPriority Priority { get; set; } public ModPriority Priority { get; set; }
@ -39,19 +38,19 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup
.SelectWhere(o => (o.Files.TryGetValue(gamePath, out var file) || o.FileSwaps.TryGetValue(gamePath, out file), file)) .SelectWhere(o => (o.Files.TryGetValue(gamePath, out var file) || o.FileSwaps.TryGetValue(gamePath, out file), file))
.FirstOrDefault(); .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) if (groupIdx < 0)
return -1; return null;
var subMod = new MultiSubMod(mod, this) var subMod = new MultiSubMod(this)
{ {
Name = name, Name = name,
Description = description, Description = description,
}; };
OptionData.Add(subMod); OptionData.Add(subMod);
return OptionData.Count - 1; return subMod;
} }
public static MultiModGroup? Load(Mod mod, JObject json) public static MultiModGroup? Load(Mod mod, JObject json)
@ -78,7 +77,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup
break; break;
} }
var subMod = new MultiSubMod(mod, ret, child); var subMod = new MultiSubMod(ret, child);
ret.OptionData.Add(subMod); ret.OptionData.Add(subMod);
} }
@ -87,12 +86,8 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup
return ret; return ret;
} }
public IModGroup Convert(GroupType type) public SingleModGroup ConvertToSingle()
{ {
switch (type)
{
case GroupType.Multi: return this;
case GroupType.Single:
var single = new SingleModGroup(Mod) var single = new SingleModGroup(Mod)
{ {
Name = Name, Name = Name,
@ -100,29 +95,12 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup
Priority = Priority, Priority = Priority,
DefaultSettings = DefaultSettings.TurnMulti(OptionData.Count), DefaultSettings = DefaultSettings.TurnMulti(OptionData.Count),
}; };
single.OptionData.AddRange(OptionData.Select(o => o.ConvertToSingle(Mod, single))); single.OptionData.AddRange(OptionData.Select(o => o.ConvertToSingle(single)));
return 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;
} }
public int GetIndex() public int GetIndex()
{ => ModGroup.GetIndex(this);
var groupIndex = Mod.Groups.IndexOf(this);
if (groupIndex < 0)
throw new Exception($"Mod {Mod.Name} from Group {Name} does not contain this group.");
return groupIndex;
}
public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations) public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations)
{ {
@ -156,15 +134,15 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup
=> ModGroup.GetCountsBase(this); => ModGroup.GetCountsBase(this);
public Setting FixSetting(Setting setting) public Setting FixSetting(Setting setting)
=> new(setting.Value & (1ul << OptionData.Count) - 1); => new(setting.Value & ((1ul << OptionData.Count) - 1));
/// <summary> Create a group without a mod only for saving it in the creator. </summary> /// <summary> Create a group without a mod only for saving it in the creator. </summary>
internal static MultiModGroup CreateForSaving(string name) internal static MultiModGroup WithoutMod(string name)
=> new(null!) => new(null!)
{ {
Name = name, Name = name,
}; };
IReadOnlyList<IModDataOption> ITexToolsGroup.OptionData IReadOnlyList<OptionSubMod> ITexToolsGroup.OptionData
=> OptionData; => OptionData;
} }

View file

@ -1,7 +1,6 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using OtterGui; using OtterGui;
using OtterGui.Filesystem;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Settings; using Penumbra.Mods.Settings;
@ -16,7 +15,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup
public GroupType Type public GroupType Type
=> GroupType.Single; => GroupType.Single;
public Mod Mod { get; set; } = mod; public Mod Mod { get; } = mod;
public string Name { get; set; } = "Option"; public string Name { get; set; } = "Option";
public string Description { get; set; } = "A mutually exclusive group of settings."; public string Description { get; set; } = "A mutually exclusive group of settings.";
public ModPriority Priority { get; set; } public ModPriority Priority { get; set; }
@ -24,23 +23,20 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup
public readonly List<SingleSubMod> OptionData = []; public readonly List<SingleSubMod> OptionData = [];
IReadOnlyList<IModDataOption> ITexToolsGroup.OptionData
=> OptionData;
public FullPath? FindBestMatch(Utf8GamePath gamePath) public FullPath? FindBestMatch(Utf8GamePath gamePath)
=> OptionData => OptionData
.SelectWhere(m => (m.Files.TryGetValue(gamePath, out var file) || m.FileSwaps.TryGetValue(gamePath, out file), file)) .SelectWhere(m => (m.Files.TryGetValue(gamePath, out var file) || m.FileSwaps.TryGetValue(gamePath, out file), file))
.FirstOrDefault(); .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, Description = description,
}; };
OptionData.Add(subMod); OptionData.Add(subMod);
return OptionData.Count - 1; return subMod;
} }
public IReadOnlyList<IModOption> Options public IReadOnlyList<IModOption> Options
@ -68,7 +64,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup
if (options != null) if (options != null)
foreach (var child in options.Children()) foreach (var child in options.Children())
{ {
var subMod = new SingleSubMod(mod, ret, child); var subMod = new SingleSubMod(ret, child);
ret.OptionData.Add(subMod); ret.OptionData.Add(subMod);
} }
@ -76,12 +72,8 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup
return ret; return ret;
} }
public IModGroup Convert(GroupType type) public MultiModGroup ConvertToMulti()
{ {
switch (type)
{
case GroupType.Single: return this;
case GroupType.Multi:
var multi = new MultiModGroup(Mod) var multi = new MultiModGroup(Mod)
{ {
Name = Name, Name = Name,
@ -89,44 +81,12 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup
Priority = Priority, Priority = Priority,
DefaultSettings = Setting.Multi((int)DefaultSettings.Value), DefaultSettings = Setting.Multi((int)DefaultSettings.Value),
}; };
multi.OptionData.AddRange(OptionData.Select((o, i) => o.ConvertToMulti(Mod, multi, new ModPriority(i)))); multi.OptionData.AddRange(OptionData.Select((o, i) => o.ConvertToMulti(multi, new ModPriority(i))));
return multi; return multi;
default: throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
}
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() public int GetIndex()
{ => ModGroup.GetIndex(this);
var groupIndex = Mod.Groups.IndexOf(this);
if (groupIndex < 0)
throw new Exception($"Mod {Mod.Name} from Group {Name} does not contain this group.");
return groupIndex;
}
public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations) public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations)
=> OptionData[setting.AsIndex].AddDataTo(redirections, manipulations); => OptionData[setting.AsIndex].AddDataTo(redirections, manipulations);
@ -160,4 +120,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup
{ {
Name = name, Name = name,
}; };
IReadOnlyList<OptionSubMod> ITexToolsGroup.OptionData
=> OptionData;
} }

View file

@ -1,3 +1,4 @@
using OtterGui.Classes;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
@ -8,6 +9,7 @@ using Penumbra.Meta;
using Penumbra.Mods.Editor; using Penumbra.Mods.Editor;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.Mods.Settings; using Penumbra.Mods.Settings;
using Penumbra.Mods.SubMods;
namespace Penumbra.Mods.ItemSwap; namespace Penumbra.Mods.ItemSwap;
@ -40,8 +42,7 @@ public class ItemSwapContainer
NoSwaps, NoSwaps,
} }
public bool WriteMod(ModManager manager, Mod mod, WriteType writeType = WriteType.NoSwaps, DirectoryInfo? directory = null, public bool WriteMod(ModManager manager, Mod mod, IModDataContainer container, WriteType writeType = WriteType.NoSwaps, DirectoryInfo? directory = null)
int groupIndex = -1, int optionIndex = 0)
{ {
var convertedManips = new HashSet<MetaManipulation>(Swaps.Count); var convertedManips = new HashSet<MetaManipulation>(Swaps.Count);
var convertedFiles = new Dictionary<Utf8GamePath, FullPath>(Swaps.Count); var convertedFiles = new Dictionary<Utf8GamePath, FullPath>(Swaps.Count);
@ -80,9 +81,9 @@ public class ItemSwapContainer
} }
} }
manager.OptionEditor.OptionSetFiles(mod, groupIndex, optionIndex, convertedFiles); manager.OptionEditor.SetFiles(container, convertedFiles, SaveType.None);
manager.OptionEditor.OptionSetFileSwaps(mod, groupIndex, optionIndex, convertedSwaps); manager.OptionEditor.SetFileSwaps(container, convertedSwaps, SaveType.None);
manager.OptionEditor.OptionSetManipulations(mod, groupIndex, optionIndex, convertedManips); manager.OptionEditor.SetManipulations(container, convertedManips, SaveType.ImmediateSync);
return true; return true;
} }
catch (Exception e) catch (Exception e)

View file

@ -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<ImcModGroup, ImcSubMod>(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;
}
}

View file

@ -2,6 +2,8 @@ using Penumbra.Communication;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Groups;
using Penumbra.Mods.SubMods;
using Penumbra.Services; using Penumbra.Services;
namespace Penumbra.Mods.Manager; 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) switch (type)
{ {

View file

@ -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;
/// <summary> Change the settings stored as default options in a mod.</summary>
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);
}
/// <summary> Rename an option group if possible. </summary>
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);
}
/// <summary> Delete a given option group. Fires an event to prepare before actually deleting. </summary>
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);
}
/// <summary> Move the index of a given option group. </summary>
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);
}
/// <summary> Change the internal priority of the given option group. </summary>
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);
}
/// <summary> Change the description of the given option group. </summary>
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);
}
/// <summary> Rename the given option. </summary>
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);
}
/// <summary> Change the description of the given option. </summary>
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);
}
/// <summary> Set the meta manipulations for a given option. Replaces existing manipulations. </summary>
public void SetManipulations(IModDataContainer subMod, HashSet<MetaManipulation> 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);
}
/// <summary> Set the file redirections for a given option. Replaces existing redirections. </summary>
public void SetFiles(IModDataContainer subMod, IReadOnlyDictionary<Utf8GamePath, FullPath> 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);
}
/// <summary> Add additional file redirections to a given option, keeping already existing ones. Only fires an event if anything is actually added.</summary>
public void AddFiles(IModDataContainer subMod, IReadOnlyDictionary<Utf8GamePath, FullPath> 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);
}
}
/// <summary> Set the file swaps for a given option. Replaces existing swaps. </summary>
public void SetFileSwaps(IModDataContainer subMod, IReadOnlyDictionary<Utf8GamePath, FullPath> 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);
}
/// <summary> Verify that a new option group name is unique in this mod. </summary>
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;
}
}
}

View file

@ -1,4 +1,3 @@
using System.Security.AccessControl;
using Penumbra.Communication; using Penumbra.Communication;
using Penumbra.Mods.Editor; using Penumbra.Mods.Editor;
using Penumbra.Services; using Penumbra.Services;
@ -34,12 +33,12 @@ public sealed class ModManager : ModStorage, IDisposable
public readonly ModCreator Creator; public readonly ModCreator Creator;
public readonly ModDataEditor DataEditor; public readonly ModDataEditor DataEditor;
public readonly ModOptionEditor OptionEditor; public readonly ModGroupEditor OptionEditor;
public DirectoryInfo BasePath { get; private set; } = null!; public DirectoryInfo BasePath { get; private set; } = null!;
public bool Valid { get; private set; } 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) ModCreator creator)
{ {
_config = config; _config = config;

View file

@ -83,8 +83,8 @@ public static partial class ModMigration
mod.Default.FileSwaps.Add(gamePath, swapPath); mod.Default.FileSwaps.Add(gamePath, swapPath);
creator.IncorporateMetaChanges(mod.Default, mod.ModPath, true); creator.IncorporateMetaChanges(mod.Default, mod.ModPath, true);
foreach (var (_, index) in mod.Groups.WithIndex()) foreach (var group in mod.Groups)
saveService.ImmediateSave(new ModSaveGroup(mod, index, creator.Config.ReplaceNonAsciiOnImport)); saveService.ImmediateSave(new ModSaveGroup(group, creator.Config.ReplaceNonAsciiOnImport));
// Delete meta files. // Delete meta files.
foreach (var file in seenMetaFiles.Where(f => f.Exists)) foreach (var file in seenMetaFiles.Where(f => f.Exists))
@ -112,7 +112,7 @@ public static partial class ModMigration
} }
fileVersion = 1; fileVersion = 1;
saveService.ImmediateSave(new ModSaveGroup(mod, -1, creator.Config.ReplaceNonAsciiOnImport)); saveService.ImmediateSave(new ModSaveGroup(mod.ModPath, mod.Default, creator.Config.ReplaceNonAsciiOnImport));
return true; return true;
} }
@ -176,7 +176,7 @@ public static partial class ModMigration
private static SingleSubMod SubModFromOption(ModCreator creator, Mod mod, SingleModGroup group, OptionV0 option, private static SingleSubMod SubModFromOption(ModCreator creator, Mod mod, SingleModGroup group, OptionV0 option,
HashSet<FullPath> seenMetaFiles) HashSet<FullPath> seenMetaFiles)
{ {
var subMod = new SingleSubMod(mod, group) var subMod = new SingleSubMod(group)
{ {
Name = option.OptionName, Name = option.OptionName,
Description = option.OptionDesc, Description = option.OptionDesc,
@ -189,7 +189,7 @@ public static partial class ModMigration
private static MultiSubMod SubModFromOption(ModCreator creator, Mod mod, MultiModGroup group, OptionV0 option, private static MultiSubMod SubModFromOption(ModCreator creator, Mod mod, MultiModGroup group, OptionV0 option,
ModPriority priority, HashSet<FullPath> seenMetaFiles) ModPriority priority, HashSet<FullPath> seenMetaFiles)
{ {
var subMod = new MultiSubMod(mod, group) var subMod = new MultiSubMod(group)
{ {
Name = option.OptionName, Name = option.OptionName,
Description = option.OptionDesc, Description = option.OptionDesc,
@ -219,7 +219,7 @@ public static partial class ModMigration
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
public GroupType SelectionType = GroupType.Single; public GroupType SelectionType = GroupType.Single;
public List<OptionV0> Options = new(); public List<OptionV0> Options = [];
public OptionGroupV0() public OptionGroupV0()
{ } { }
@ -236,12 +236,12 @@ public static partial class ModMigration
var token = JToken.Load(reader); var token = JToken.Load(reader);
if (token.Type == JTokenType.Array) if (token.Type == JTokenType.Array)
return token.ToObject<HashSet<T>>() ?? new HashSet<T>(); return token.ToObject<HashSet<T>>() ?? [];
var tmp = token.ToObject<T>(); var tmp = token.ToObject<T>();
return tmp != null return tmp != null
? new HashSet<T> { tmp } ? new HashSet<T> { tmp }
: new HashSet<T>(); : [];
} }
public override bool CanWrite public override bool CanWrite

View file

@ -1,384 +1,122 @@
using Dalamud.Interface.Internal.Notifications;
using OtterGui; using OtterGui;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Filesystem;
using Penumbra.Api.Enums;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Groups; using Penumbra.Mods.Groups;
using Penumbra.Mods.Settings; using Penumbra.Mods.Settings;
using Penumbra.Mods.SubMods; using Penumbra.Mods.SubMods;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.String.Classes;
using Penumbra.Util;
namespace Penumbra.Mods.Manager; namespace Penumbra.Mods.Manager;
public enum ModOptionChangeType public abstract class ModOptionEditor<TGroup, TOption>(
CommunicatorService communicator,
SaveService saveService,
Configuration config)
where TGroup : class, IModGroup
where TOption : class, IModOption
{ {
GroupRenamed, protected readonly CommunicatorService Communicator = communicator;
GroupAdded, protected readonly SaveService SaveService = saveService;
GroupDeleted, protected readonly Configuration Config = config;
GroupMoved,
GroupTypeChanged,
PriorityChanged,
OptionAdded,
OptionDeleted,
OptionMoved,
OptionFilesChanged,
OptionFilesAdded,
OptionSwapsChanged,
OptionMetaChanged,
DisplayChange,
PrepareChange,
DefaultOptionChanged,
}
public class ModOptionEditor(CommunicatorService communicator, SaveService saveService, Configuration config)
{
/// <summary> Change the type of a group given by mod and index to type, if possible. </summary>
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);
}
/// <summary> Change the settings stored as default options in a mod.</summary>
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);
}
/// <summary> Rename an option group if possible. </summary>
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);
}
/// <summary> Add a new, empty option group of the given type and name. </summary> /// <summary> Add a new, empty option group of the given type and name. </summary>
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)) if (!ModGroupEditor.VerifyFileName(mod, null, newName, true))
return; return null;
var idx = mod.Groups.Count; var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1;
var group = ModGroup.Create(mod, type, newName); var group = CreateGroup(mod, newName, maxPriority);
mod.Groups.Add(group); mod.Groups.Add(group);
saveService.Save(saveType, new ModSaveGroup(mod, idx, config.ReplaceNonAsciiOnImport)); SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, idx, -1, -1); Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, group, null, null, -1);
return group;
} }
/// <summary> Add a new mod, empty option group of the given type and name if it does not exist already. </summary> /// <summary> Add a new mod, empty option group of the given type and name if it does not exist already. </summary>
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); var idx = mod.Groups.IndexOf(g => g.Name == newName);
if (idx >= 0) 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); idx = mod.Groups.Count;
if (mod.Groups[^1].Name != newName) if (AddModGroup(mod, newName, saveType) is not { } group)
throw new Exception($"Could not create new mod group with name {newName}."); throw new Exception($"Could not create new mod group with name {newName}.");
return (mod.Groups[^1], mod.Groups.Count - 1, true); return (group, idx, true);
}
/// <summary> Delete a given option group. Fires an event to prepare before actually deleting. </summary>
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);
}
/// <summary> Move the index of a given option group. </summary>
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);
}
/// <summary> Change the description of the given option group. </summary>
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);
}
/// <summary> Change the description of the given option. </summary>
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);
}
/// <summary> Change the internal priority of the given option group. </summary>
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);
}
/// <summary> Change the internal priority of the given option. </summary>
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;
}
}
/// <summary> Rename the given option. </summary>
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);
} }
/// <summary> Add a new empty option of the given name for the given group. </summary> /// <summary> Add a new empty option of the given name for the given group. </summary>
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]; if (group.AddOption(newName) is not TOption option)
var idx = group.AddOption(mod, newName); return null;
if (idx < 0)
return -1;
saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1); Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, group.Mod, group, option, null, -1);
return idx; return option;
} }
/// <summary> Add a new empty option of the given name for the given group if it does not exist already. </summary> /// <summary> Add a new empty option of the given name for the given group if it does not exist already. </summary>
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) 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 (AddOption(group, newName, saveType) is not { } option)
if (idx < 0)
throw new Exception($"Could not create new option with name {newName} in {group.Name}."); throw new Exception($"Could not create new option with name {newName} in {group.Name}.");
saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); return (option, idx, true);
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1);
return (group.Options[idx], idx, true);
} }
/// <summary> Add an existing option to a given group. </summary> /// <summary> Add an existing option to a given group. </summary>
public void AddOption(Mod mod, int groupIdx, IModOption option) public TOption? AddOption(TGroup group, IModOption option)
{ {
var group = mod.Groups[groupIdx]; if (CloneOption(group, option) is not { } clonedOption)
int idx; return null;
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;
}
saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1); Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, group.Mod, group, clonedOption, null, -1);
return clonedOption;
} }
/// <summary> Delete the given option from the given group. </summary> /// <summary> Delete the given option from the given group. </summary>
public void DeleteOption(Mod mod, int groupIdx, int optionIdx) public void DeleteOption(TOption option)
{ {
var group = mod.Groups[groupIdx]; var mod = option.Mod;
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); var group = option.Group;
switch (group) var optionIdx = option.GetIndex();
{ Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, group, option, null, -1);
case SingleModGroup s: RemoveOption((TGroup)group, optionIdx);
s.OptionData.RemoveAt(optionIdx); SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport));
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, group, null, null, 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);
} }
/// <summary> Move an option inside the given option group. </summary> /// <summary> Move an option inside the given option group. </summary>
public void MoveOption(Mod mod, int groupIdx, int optionIdxFrom, int optionIdxTo) public void MoveOption(TOption option, int optionIdxTo)
{ {
var group = mod.Groups[groupIdx]; var idx = option.GetIndex();
if (!group.MoveOption(optionIdxFrom, optionIdxTo)) var group = (TGroup)option.Group;
if (!MoveOption(group, idx, optionIdxTo))
return; return;
saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, mod, groupIdx, optionIdxFrom, optionIdxTo); Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, group.Mod, group, option, null, idx);
} }
/// <summary> Set the meta manipulations for a given option. Replaces existing manipulations. </summary> protected abstract TGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync);
public void OptionSetManipulations(Mod mod, int groupIdx, int dataContainerIdx, HashSet<MetaManipulation> manipulations, protected abstract TOption? CloneOption(TGroup group, IModOption option);
SaveType saveType = SaveType.Queue) protected abstract void RemoveOption(TGroup group, int optionIndex);
{ protected abstract bool MoveOption(TGroup group, int optionIdxFrom, int optionIdxTo);
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);
}
/// <summary> Set the file redirections for a given option. Replaces existing redirections. </summary>
public void OptionSetFiles(Mod mod, int groupIdx, int dataContainerIdx, IReadOnlyDictionary<Utf8GamePath, FullPath> 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);
}
/// <summary> Add additional file redirections to a given option, keeping already existing ones. Only fires an event if anything is actually added.</summary>
public void OptionAddFiles(Mod mod, int groupIdx, int dataContainerIdx, IReadOnlyDictionary<Utf8GamePath, FullPath> 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);
}
}
/// <summary> Set the file swaps for a given option. Replaces existing swaps. </summary>
public void OptionSetFileSwaps(Mod mod, int groupIdx, int dataContainerIdx, IReadOnlyDictionary<Utf8GamePath, FullPath> 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);
}
/// <summary> Verify that a new option group name is unique in this mod. </summary>
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;
}
/// <summary> Get the correct option for the given group and option index. </summary>
private static IModDataContainer GetSubMod(Mod mod, int groupIdx, int dataContainerIdx)
{
if (groupIdx == -1 && dataContainerIdx == 0)
return mod.Default;
return mod.Groups[groupIdx].DataContainers[dataContainerIdx];
}
} }
public static class ModOptionChangeTypeExtension public static class ModOptionChangeTypeExtension

View file

@ -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<MultiModGroup, MultiSubMod>(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);
}
/// <summary> Change the internal priority of the given option. </summary>
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;
}
}

View file

@ -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<SingleModGroup, SingleSubMod>(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;
}
}

View file

@ -90,7 +90,7 @@ public partial class ModCreator(
var changes = false; var changes = false;
foreach (var file in _saveService.FileNames.GetOptionGroupFiles(mod)) 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)) if (group != null && mod.Groups.All(g => g.Name != group.Name))
{ {
changes = changes changes = changes
@ -244,12 +244,12 @@ public partial class ModCreator(
{ {
case GroupType.Multi: case GroupType.Multi:
{ {
var group = MultiModGroup.CreateForSaving(name); var group = MultiModGroup.WithoutMod(name);
group.Description = desc; group.Description = desc;
group.Priority = priority; group.Priority = priority;
group.DefaultSettings = defaultSettings; group.DefaultSettings = defaultSettings;
group.OptionData.AddRange(subMods.Select(s => s.Clone(null!, group))); group.OptionData.AddRange(subMods.Select(s => s.Clone(group)));
_saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); _saveService.ImmediateSaveSync(ModSaveGroup.WithoutMod(baseFolder, group, index, Config.ReplaceNonAsciiOnImport));
break; break;
} }
case GroupType.Single: case GroupType.Single:
@ -258,8 +258,8 @@ public partial class ModCreator(
group.Description = desc; group.Description = desc;
group.Priority = priority; group.Priority = priority;
group.DefaultSettings = defaultSettings; group.DefaultSettings = defaultSettings;
group.OptionData.AddRange(subMods.Select(s => s.ConvertToSingle(null!, group))); group.OptionData.AddRange(subMods.Select(s => s.ConvertToSingle(group)));
_saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); _saveService.ImmediateSaveSync(ModSaveGroup.WithoutMod(baseFolder, group, index, Config.ReplaceNonAsciiOnImport));
break; break;
} }
} }
@ -272,7 +272,7 @@ public partial class ModCreator(
.Select(f => (Utf8GamePath.FromFile(f, optionFolder, out var gamePath, true), gamePath, new FullPath(f))) .Select(f => (Utf8GamePath.FromFile(f, optionFolder, out var gamePath, true), gamePath, new FullPath(f)))
.Where(t => t.Item1); .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) foreach (var (_, gamePath, file) in list)
mod.Files.TryAdd(gamePath, file); mod.Files.TryAdd(gamePath, file);
@ -295,7 +295,7 @@ public partial class ModCreator(
} }
IncorporateMetaChanges(mod.Default, directory, true); IncorporateMetaChanges(mod.Default, directory, true);
_saveService.ImmediateSaveSync(new ModSaveGroup(mod, -1, Config.ReplaceNonAsciiOnImport)); _saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, mod.Default, Config.ReplaceNonAsciiOnImport));
} }
/// <summary> Return the name of a new valid directory based on the base directory and the given name. </summary> /// <summary> Return the name of a new valid directory based on the base directory and the given name. </summary>
@ -422,7 +422,7 @@ public partial class ModCreator(
/// <summary> Load an option group for a specific mod by its file and index. </summary> /// <summary> Load an option group for a specific mod by its file and index. </summary>
private static IModGroup? LoadModGroup(Mod mod, FileInfo file, int groupIdx) private static IModGroup? LoadModGroup(Mod mod, FileInfo file)
{ {
if (!File.Exists(file.FullName)) if (!File.Exists(file.FullName))
return null; return null;

View file

@ -4,6 +4,7 @@ using Penumbra.Api.Enums;
using Penumbra.Mods.Editor; using Penumbra.Mods.Editor;
using Penumbra.Mods.Groups; using Penumbra.Mods.Groups;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.Mods.SubMods;
namespace Penumbra.Mods.Settings; namespace Penumbra.Mods.Settings;
@ -45,63 +46,64 @@ public class ModSettings
} }
// Automatically react to changes in a mods available options. // 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) switch (type)
{ {
case ModOptionChangeType.GroupRenamed: return true; case ModOptionChangeType.GroupRenamed: return true;
case ModOptionChangeType.GroupAdded: case ModOptionChangeType.GroupAdded:
// Add new empty setting for new mod. // Add new empty setting for new mod.
Settings.Insert(groupIdx, mod.Groups[groupIdx].DefaultSettings); Settings.Insert(group!.GetIndex(), group.DefaultSettings);
return true; return true;
case ModOptionChangeType.GroupDeleted: case ModOptionChangeType.GroupDeleted:
// Remove setting for deleted mod. // Remove setting for deleted mod.
Settings.RemoveAt(groupIdx); Settings.RemoveAt(fromIdx);
return true; return true;
case ModOptionChangeType.GroupTypeChanged: case ModOptionChangeType.GroupTypeChanged:
{ {
// Fix settings for a changed group type. // Fix settings for a changed group type.
// Single -> Multi: set single as enabled, rest as disabled // Single -> Multi: set single as enabled, rest as disabled
// Multi -> Single: set the first enabled option or 0. // Multi -> Single: set the first enabled option or 0.
var group = mod.Groups[groupIdx]; var idx = group!.GetIndex();
var config = Settings[groupIdx]; var config = Settings[idx];
Settings[groupIdx] = group.Type switch Settings[idx] = group.Type switch
{ {
GroupType.Single => config.TurnMulti(group.Options.Count), GroupType.Single => config.TurnMulti(group.Options.Count),
GroupType.Multi => Setting.Multi((int)config.Value), GroupType.Multi => Setting.Multi((int)config.Value),
_ => config, _ => config,
}; };
return config != Settings[groupIdx]; return config != Settings[idx];
} }
case ModOptionChangeType.OptionDeleted: case ModOptionChangeType.OptionDeleted:
{ {
// Single -> select the previous option if any. // Single -> select the previous option if any.
// Multi -> excise the corresponding bit. // Multi -> excise the corresponding bit.
var group = mod.Groups[groupIdx]; var groupIdx = group!.GetIndex();
var config = Settings[groupIdx]; var config = Settings[groupIdx];
Settings[groupIdx] = group.Type switch Settings[groupIdx] = group!.Type switch
{ {
GroupType.Single => config.AsIndex >= optionIdx GroupType.Single => config.RemoveSingle(fromIdx),
? config.AsIndex > 1 ? Setting.Single(config.AsIndex - 1) : Setting.Zero GroupType.Multi => config.RemoveBit(fromIdx),
: config, GroupType.Imc => config.RemoveBit(fromIdx),
GroupType.Multi => config.RemoveBit(optionIdx),
_ => config, _ => config,
}; };
return config != Settings[groupIdx]; return config != Settings[groupIdx];
} }
case ModOptionChangeType.GroupMoved: case ModOptionChangeType.GroupMoved:
// Move the group the same way. // Move the group the same way.
return Settings.Move(groupIdx, movedToIdx); return Settings.Move(fromIdx, group!.GetIndex());
case ModOptionChangeType.OptionMoved: case ModOptionChangeType.OptionMoved:
{ {
// Single -> select the moved option if it was currently selected // Single -> select the moved option if it was currently selected
// Multi -> move the corresponding bit // Multi -> move the corresponding bit
var group = mod.Groups[groupIdx]; var groupIdx = group!.GetIndex();
var toIdx = option!.GetIndex();
var config = Settings[groupIdx]; var config = Settings[groupIdx];
Settings[groupIdx] = group.Type switch Settings[groupIdx] = group!.Type switch
{ {
GroupType.Single => config.AsIndex == optionIdx ? Setting.Single(movedToIdx) : config, GroupType.Single => config.MoveSingle(fromIdx, toIdx),
GroupType.Multi => config.MoveBit(optionIdx, movedToIdx), GroupType.Multi => config.MoveBit(fromIdx, toIdx),
GroupType.Imc => config.MoveBit(fromIdx, toIdx),
_ => config, _ => config,
}; };
return config != Settings[groupIdx]; return config != Settings[groupIdx];

View file

@ -41,6 +41,34 @@ public readonly record struct Setting(ulong Value)
public Setting TurnMulti(int count) public Setting TurnMulti(int count)
=> new(Math.Max((ulong)Math.Min(count - 1, BitOperations.TrailingZeroCount(Value)), 0)); => 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 public ModPriority AsPriority
=> new((int)(Value & 0xFFFFFFFF)); => new((int)(Value & 0xFFFFFFFF));

View file

@ -1,10 +1,15 @@
using Penumbra.Mods.Groups;
namespace Penumbra.Mods.SubMods; namespace Penumbra.Mods.SubMods;
public interface IModOption public interface IModOption
{ {
public Mod Mod { get; }
public IModGroup Group { get; }
public string Name { get; set; } public string Name { get; set; }
public string FullName { get; } public string FullName { get; }
public string Description { get; set; } public string Description { get; set; }
public (int GroupIndex, int OptionIndex) GetOptionIndices(); public int GetIndex();
} }

View file

@ -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);
}

View file

@ -4,21 +4,21 @@ using Penumbra.Mods.Settings;
namespace Penumbra.Mods.SubMods; namespace Penumbra.Mods.SubMods;
public class MultiSubMod(Mod mod, MultiModGroup group) : OptionSubMod<MultiModGroup>(mod, group) public class MultiSubMod(MultiModGroup group) : OptionSubMod<MultiModGroup>(group)
{ {
public ModPriority Priority { get; set; } = ModPriority.Default; public ModPriority Priority { get; set; } = ModPriority.Default;
public MultiSubMod(Mod mod, MultiModGroup group, JToken json) public MultiSubMod(MultiModGroup group, JToken json)
: this(mod, group) : this(group)
{ {
SubMod.LoadOptionData(json, this); SubMod.LoadOptionData(json, this);
SubMod.LoadDataContainer(json, this, mod.ModPath); SubMod.LoadDataContainer(json, this, group.Mod.ModPath);
Priority = json[nameof(IModGroup.Priority)]?.ToObject<ModPriority>() ?? ModPriority.Default; Priority = json[nameof(IModGroup.Priority)]?.ToObject<ModPriority>() ?? 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, Name = Name,
Description = Description, Description = Description,
@ -29,9 +29,9 @@ public class MultiSubMod(Mod mod, MultiModGroup group) : OptionSubMod<MultiModGr
return ret; return ret;
} }
public SingleSubMod ConvertToSingle(Mod mod, SingleModGroup group) public SingleSubMod ConvertToSingle(SingleModGroup group)
{ {
var ret = new SingleSubMod(mod, group) var ret = new SingleSubMod(group)
{ {
Name = Name, Name = Name,
Description = Description, Description = Description,
@ -40,8 +40,8 @@ public class MultiSubMod(Mod mod, MultiModGroup group) : OptionSubMod<MultiModGr
return ret; return ret;
} }
public static MultiSubMod CreateForSaving(string name, string description, ModPriority priority) public static MultiSubMod WithoutGroup(string name, string description, ModPriority priority)
=> new(null!, null!) => new(null!)
{ {
Name = name, Name = name,
Description = description, Description = description,

View file

@ -6,20 +6,21 @@ using Penumbra.String.Classes;
namespace Penumbra.Mods.SubMods; namespace Penumbra.Mods.SubMods;
public interface IModDataOption : IModDataContainer, IModOption; public abstract class OptionSubMod(IModGroup group) : IModOption, IModDataContainer
public abstract class OptionSubMod<T>(Mod mod, T group) : IModDataOption
where T : IModGroup
{ {
internal readonly Mod Mod = mod; protected readonly IModGroup Group = group;
internal readonly IModGroup Group = group;
public Mod Mod
=> Group.Mod;
public string Name { get; set; } = "Option"; public string Name { get; set; } = "Option";
public string Description { get; set; } = string.Empty;
public string FullName public string FullName
=> $"{Group!.Name}: {Name}"; => $"{Group.Name}: {Name}";
public string Description { get; set; } = string.Empty; Mod IModOption.Mod
=> Mod;
IMod IModDataContainer.Mod IMod IModDataContainer.Mod
=> Mod; => Mod;
@ -27,6 +28,9 @@ public abstract class OptionSubMod<T>(Mod mod, T group) : IModDataOption
IModGroup IModDataContainer.Group IModGroup IModDataContainer.Group
=> Group; => Group;
IModGroup IModOption.Group
=> Group;
public Dictionary<Utf8GamePath, FullPath> Files { get; set; } = []; public Dictionary<Utf8GamePath, FullPath> Files { get; set; } = [];
public Dictionary<Utf8GamePath, FullPath> FileSwaps { get; set; } = []; public Dictionary<Utf8GamePath, FullPath> FileSwaps { get; set; } = [];
public HashSet<MetaManipulation> Manipulations { get; set; } = []; public HashSet<MetaManipulation> Manipulations { get; set; } = [];
@ -43,8 +47,8 @@ public abstract class OptionSubMod<T>(Mod mod, T group) : IModDataOption
public (int GroupIndex, int DataIndex) GetDataIndices() public (int GroupIndex, int DataIndex) GetDataIndices()
=> (Group.GetIndex(), GetDataIndex()); => (Group.GetIndex(), GetDataIndex());
public (int GroupIndex, int OptionIndex) GetOptionIndices() public int GetIndex()
=> (Group.GetIndex(), GetDataIndex()); => SubMod.GetIndex(this);
private int GetDataIndex() private int GetDataIndex()
{ {
@ -55,3 +59,10 @@ public abstract class OptionSubMod<T>(Mod mod, T group) : IModDataOption
return dataIndex; return dataIndex;
} }
} }
public abstract class OptionSubMod<T>(T group) : OptionSubMod(group)
where T : IModGroup
{
public new T Group
=> (T)base.Group;
}

View file

@ -4,18 +4,18 @@ using Penumbra.Mods.Settings;
namespace Penumbra.Mods.SubMods; namespace Penumbra.Mods.SubMods;
public class SingleSubMod(Mod mod, SingleModGroup singleGroup) : OptionSubMod<SingleModGroup>(mod, singleGroup) public class SingleSubMod(SingleModGroup singleGroup) : OptionSubMod<SingleModGroup>(singleGroup)
{ {
public SingleSubMod(Mod mod, SingleModGroup singleGroup, JToken json) public SingleSubMod(SingleModGroup singleGroup, JToken json)
: this(mod, singleGroup) : this(singleGroup)
{ {
SubMod.LoadOptionData(json, this); 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, Name = Name,
Description = Description, Description = Description,
@ -25,9 +25,9 @@ public class SingleSubMod(Mod mod, SingleModGroup singleGroup) : OptionSubMod<Si
return ret; return ret;
} }
public MultiSubMod ConvertToMulti(Mod mod, MultiModGroup group, ModPriority priority) public MultiSubMod ConvertToMulti(MultiModGroup group, ModPriority priority)
{ {
var ret = new MultiSubMod(mod, group) var ret = new MultiSubMod(group)
{ {
Name = Name, Name = Name,
Description = Description, Description = Description,

View file

@ -1,30 +1,25 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using OtterGui;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Groups;
using Penumbra.String.Classes; using Penumbra.String.Classes;
namespace Penumbra.Mods.SubMods; namespace Penumbra.Mods.SubMods;
public static class SubMod public static class SubMod
{ {
public static IModOption Create(IModGroup group, string name, string description = "") [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
=> group switch public static int GetIndex(IModOption option)
{ {
SingleModGroup single => new SingleSubMod(group.Mod, single) var dataIndex = option.Group.Options.IndexOf(option);
{ if (dataIndex < 0)
Name = name, throw new Exception($"Group {option.Group.Name} from option {option.Name} does not contain this option.");
Description = description,
}, return dataIndex;
MultiModGroup multi => new MultiSubMod(group.Mod, multi) }
{
Name = name,
Description = description,
},
_ => throw new ArgumentOutOfRangeException(nameof(group)),
};
/// <summary> 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. </summary> /// <summary> 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. </summary>
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
public static void AddContainerTo(IModDataContainer container, Dictionary<Utf8GamePath, FullPath> redirections, public static void AddContainerTo(IModDataContainer container, Dictionary<Utf8GamePath, FullPath> redirections,
HashSet<MetaManipulation> manipulations) HashSet<MetaManipulation> manipulations)
{ {
@ -37,6 +32,7 @@ public static class SubMod
} }
/// <summary> Replace all data of <paramref name="to"/> with the data of <paramref name="from"/>. </summary> /// <summary> Replace all data of <paramref name="to"/> with the data of <paramref name="from"/>. </summary>
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
public static void Clone(IModDataContainer from, IModDataContainer to) public static void Clone(IModDataContainer from, IModDataContainer to)
{ {
to.Files = new Dictionary<Utf8GamePath, FullPath>(from.Files); to.Files = new Dictionary<Utf8GamePath, FullPath>(from.Files);
@ -45,6 +41,7 @@ public static class SubMod
} }
/// <summary> Load all file redirections, file swaps and meta manipulations from a JToken of that option into a data container. </summary> /// <summary> Load all file redirections, file swaps and meta manipulations from a JToken of that option into a data container. </summary>
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
public static void LoadDataContainer(JToken json, IModDataContainer data, DirectoryInfo basePath) public static void LoadDataContainer(JToken json, IModDataContainer data, DirectoryInfo basePath)
{ {
data.Files.Clear(); data.Files.Clear();
@ -75,6 +72,7 @@ public static class SubMod
} }
/// <summary> Load the relevant data for a selectable option from a JToken of that option. </summary> /// <summary> Load the relevant data for a selectable option from a JToken of that option. </summary>
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
public static void LoadOptionData(JToken json, IModOption option) public static void LoadOptionData(JToken json, IModOption option)
{ {
option.Name = json[nameof(option.Name)]?.ToObject<string>() ?? string.Empty; option.Name = json[nameof(option.Name)]?.ToObject<string>() ?? string.Empty;
@ -82,6 +80,7 @@ public static class SubMod
} }
/// <summary> Write file redirections, file swaps and meta manipulations from a data container on a JsonWriter. </summary> /// <summary> Write file redirections, file swaps and meta manipulations from a data container on a JsonWriter. </summary>
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
public static void WriteModContainer(JsonWriter j, JsonSerializer serializer, IModDataContainer data, DirectoryInfo basePath) public static void WriteModContainer(JsonWriter j, JsonSerializer serializer, IModDataContainer data, DirectoryInfo basePath)
{ {
j.WritePropertyName(nameof(data.Files)); j.WritePropertyName(nameof(data.Files));
@ -111,6 +110,7 @@ public static class SubMod
} }
/// <summary> Write the data for a selectable mod option on a JsonWriter. </summary> /// <summary> Write the data for a selectable mod option on a JsonWriter. </summary>
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
public static void WriteModOption(JsonWriter j, IModOption option) public static void WriteModOption(JsonWriter j, IModOption option)
{ {
j.WritePropertyName(nameof(option.Name)); j.WritePropertyName(nameof(option.Name));

View file

@ -23,6 +23,12 @@
<DefineConstants>PROFILING;</DefineConstants> <DefineConstants>PROFILING;</DefineConstants>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Compile Remove="Mods\Subclasses\**" />
<EmbeddedResource Remove="Mods\Subclasses\**" />
<None Remove="Mods\Subclasses\**" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Content Include="tsmLogo.png"> <Content Include="tsmLogo.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@ -93,10 +99,6 @@
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Mods\Subclasses\" />
</ItemGroup>
<Target Name="GetGitHash" BeforeTargets="GetAssemblyVersion" Returns="InformationalVersion"> <Target Name="GetGitHash" BeforeTargets="GetAssemblyVersion" Returns="InformationalVersion">
<Exec Command="git rev-parse --short HEAD" ConsoleToMSBuild="true" StandardOutputImportance="low" ContinueOnError="true"> <Exec Command="git rev-parse --short HEAD" ConsoleToMSBuild="true" StandardOutputImportance="low" ContinueOnError="true">
<Output TaskParameter="ExitCode" PropertyName="GitCommitHashSuccess" /> <Output TaskParameter="ExitCode" PropertyName="GitCommitHashSuccess" />

View file

@ -34,8 +34,11 @@ public sealed class SaveService(Logger log, FrameworkManager framework, Filename
} }
} }
for (var i = 0; i < mod.Groups.Count - 1; ++i) if (mod.Groups.Count > 0)
ImmediateSave(new ModSaveGroup(mod, i, onlyAscii)); {
ImmediateSaveSync(new ModSaveGroup(mod, mod.Groups.Count - 1, onlyAscii)); foreach (var group in mod.Groups.SkipLast(1))
ImmediateSave(new ModSaveGroup(group, onlyAscii));
ImmediateSaveSync(new ModSaveGroup(mod.Groups[^1], onlyAscii));
}
} }
} }

View file

@ -121,7 +121,6 @@ public static class StaticServiceManager
private static ServiceManager AddMods(this ServiceManager services) private static ServiceManager AddMods(this ServiceManager services)
=> services.AddSingleton<TempModManager>() => services.AddSingleton<TempModManager>()
.AddSingleton<ModDataEditor>() .AddSingleton<ModDataEditor>()
.AddSingleton<ModOptionEditor>()
.AddSingleton<ModCreator>() .AddSingleton<ModCreator>()
.AddSingleton<ModManager>() .AddSingleton<ModManager>()
.AddSingleton<ModExportManager>() .AddSingleton<ModExportManager>()

View file

@ -17,6 +17,7 @@ using Penumbra.Mods.Groups;
using Penumbra.Mods.ItemSwap; using Penumbra.Mods.ItemSwap;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.Mods.Settings; using Penumbra.Mods.Settings;
using Penumbra.Mods.SubMods;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
@ -264,9 +265,10 @@ public class ItemSwapTab : IDisposable, ITab
return; return;
_modManager.AddMod(newDir); _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)) _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps))
_modManager.DeleteMod(_modManager[^1]); _modManager.DeleteMod(mod);
} }
private void CreateOption() private void CreateOption()
@ -276,7 +278,7 @@ public class ItemSwapTab : IDisposable, ITab
var groupCreated = false; var groupCreated = false;
var dirCreated = false; var dirCreated = false;
var optionCreated = -1; IModOption? createdOption = null;
DirectoryInfo? optionFolderName = null; DirectoryInfo? optionFolderName = null;
try try
{ {
@ -290,22 +292,22 @@ public class ItemSwapTab : IDisposable, ITab
{ {
if (_selectedGroup == null) if (_selectedGroup == null)
{ {
_modManager.OptionEditor.AddModGroup(_mod, GroupType.Multi, _newGroupName); if (_modManager.OptionEditor.AddModGroup(_mod, GroupType.Multi, _newGroupName) is not { } group)
_selectedGroup = _mod.Groups.Last(); throw new Exception($"Failure creating option group.");
_selectedGroup = group;
groupCreated = true; groupCreated = true;
} }
var optionIdx = _modManager.OptionEditor.AddOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _newOptionName); if (_modManager.OptionEditor.AddOption(_selectedGroup, _newOptionName) is not { } option)
if (optionIdx < 0)
throw new Exception($"Failure creating mod option."); throw new Exception($"Failure creating mod option.");
optionCreated = optionIdx; createdOption = option;
optionFolderName = Directory.CreateDirectory(optionFolderName.FullName); optionFolderName = Directory.CreateDirectory(optionFolderName.FullName);
dirCreated = true; dirCreated = true;
if (!_swapData.WriteMod(_modManager, _mod, // #TODO ModOption <> DataContainer
_useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps, if (!_swapData.WriteMod(_modManager, _mod, (IModDataContainer)option,
optionFolderName, _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps, optionFolderName))
_mod.Groups.IndexOf(_selectedGroup), optionIdx))
throw new Exception("Failure writing files for mod swap."); 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); Penumbra.Messager.NotificationMessage(e, "Could not create new Swap Option.", NotificationType.Error, false);
try try
{ {
if (optionCreated >= 0 && _selectedGroup != null) if (createdOption != null)
_modManager.OptionEditor.DeleteOption(_mod, _mod.Groups.IndexOf(_selectedGroup), optionCreated); _modManager.OptionEditor.DeleteOption(createdOption);
if (groupCreated) if (groupCreated)
{ {
_modManager.OptionEditor.DeleteModGroup(_mod, _mod.Groups.IndexOf(_selectedGroup!)); _modManager.OptionEditor.DeleteModGroup(_selectedGroup!);
_selectedGroup = null; _selectedGroup = null;
} }
@ -717,7 +719,8 @@ public class ItemSwapTab : IDisposable, ITab
_dirty = true; _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) if (type is ModOptionChangeType.PrepareChange or ModOptionChangeType.GroupAdded or ModOptionChangeType.OptionAdded || mod != _mod)
return; return;

View file

@ -27,7 +27,7 @@ public partial class ModEditWindow
private const string GenderTooltip = "Gender"; private const string GenderTooltip = "Gender";
private const string ObjectTypeTooltip = "Object Type"; private const string ObjectTypeTooltip = "Object Type";
private const string SecondaryIdTooltip = "Secondary ID"; 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 VariantIdTooltip = "Variant ID";
private const string EstTypeTooltip = "EST Type"; private const string EstTypeTooltip = "EST Type";
private const string RacialTribeTooltip = "Racial Tribe"; 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."; var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option.";
ImGui.NewLine(); ImGui.NewLine();
if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, setsEqual)) 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(); ImGui.SameLine();
tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; tt = setsEqual ? "No changes staged." : "Revert all currently staged changes.";
@ -477,7 +477,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.PrimaryId.ToString()); ImGui.TextUnformatted(meta.PrimaryId.ToString());
ImGuiUtil.HoverTooltip(PrimaryIDTooltip); ImGuiUtil.HoverTooltip(PrimaryIdTooltipShort);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);

View file

@ -455,7 +455,7 @@ public partial class ModEditWindow : Window, IDisposable
var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option."; var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option.";
ImGui.NewLine(); ImGui.NewLine();
if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, setsEqual)) 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(); ImGui.SameLine();
tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; tt = setsEqual ? "No changes staged." : "Revert all currently staged changes.";
@ -627,7 +627,7 @@ public partial class ModEditWindow : Window, IDisposable
public void Dispose() public void Dispose()
{ {
_communicator.ModPathChanged.Unsubscribe(OnModPathChange); _communicator.ModPathChanged.Unsubscribe(OnModPathChange);
_editor?.Dispose(); _editor.Dispose();
_materialTab.Dispose(); _materialTab.Dispose();
_modelTab.Dispose(); _modelTab.Dispose();
_shaderPackageTab.Dispose(); _shaderPackageTab.Dispose();

View file

@ -15,6 +15,7 @@ using Penumbra.Services;
using Penumbra.UI.AdvancedWindow; using Penumbra.UI.AdvancedWindow;
using Penumbra.Mods.Groups; using Penumbra.Mods.Groups;
using Penumbra.Mods.Settings; using Penumbra.Mods.Settings;
using Penumbra.Mods.SubMods;
namespace Penumbra.UI.ModsTab; namespace Penumbra.UI.ModsTab;
@ -248,13 +249,13 @@ public class ModPanelEditTab(
ImGui.SameLine(); 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."; 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, if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize,
tt, !nameValid, true)) tt, !nameValid, true))
return; return;
modManager.OptionEditor.AddModGroup(mod, GroupType.Single, _newGroupName); modManager.OptionEditor.SingleEditor.AddModGroup(mod, _newGroupName);
Reset(); Reset();
} }
} }
@ -364,9 +365,9 @@ public class ModPanelEditTab(
break; break;
case >= 0: case >= 0:
if (_newDescriptionOptionIdx < 0) if (_newDescriptionOptionIdx < 0)
modManager.OptionEditor.ChangeGroupDescription(_mod, _newDescriptionIdx, _newDescription); modManager.OptionEditor.ChangeGroupDescription(_mod.Groups[_newDescriptionIdx], _newDescription);
else else
modManager.OptionEditor.ChangeOptionDescription(_mod, _newDescriptionIdx, _newDescriptionOptionIdx, modManager.OptionEditor.ChangeOptionDescription(_mod.Groups[_newDescriptionIdx].Options[_newDescriptionOptionIdx],
_newDescription); _newDescription);
break; break;
@ -396,18 +397,18 @@ public class ModPanelEditTab(
.Push(ImGuiStyleVar.ItemSpacing, _itemSpacing); .Push(ImGuiStyleVar.ItemSpacing, _itemSpacing);
if (Input.Text("##Name", groupIdx, Input.None, group.Name, out var newGroupName, 256, UiHelpers.InputTextWidth.X)) 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"); ImGuiUtil.HoverTooltip("Group Name");
ImGui.SameLine(); ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize,
"Delete this option group.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true)) "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(); ImGui.SameLine();
if (Input.Priority("##Priority", groupIdx, Input.None, group.Priority, out var priority, 50 * UiHelpers.Scale)) 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"); 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}."; 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, if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowUp.ToIconString(), UiHelpers.IconButtonSize,
tt, groupIdx == 0, true)) tt, groupIdx == 0, true))
_delayedActions.Enqueue(() => _modManager.OptionEditor.MoveModGroup(_mod, groupIdx, groupIdx - 1)); _delayedActions.Enqueue(() => _modManager.OptionEditor.MoveModGroup(group, groupIdx - 1));
ImGui.SameLine(); ImGui.SameLine();
tt = groupIdx == _mod.Groups.Count - 1 tt = groupIdx == _mod.Groups.Count - 1
@ -425,7 +426,7 @@ public class ModPanelEditTab(
: $"Move this group down to group {groupIdx + 2}."; : $"Move this group down to group {groupIdx + 2}.";
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowDown.ToIconString(), UiHelpers.IconButtonSize, if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowDown.ToIconString(), UiHelpers.IconButtonSize,
tt, groupIdx == _mod.Groups.Count - 1, true)) 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(); ImGui.SameLine();
@ -454,15 +455,15 @@ public class ModPanelEditTab(
private static int _newOptionNameIdx = -1; private static int _newOptionNameIdx = -1;
private static string _newOptionName = string.Empty; private static string _newOptionName = string.Empty;
private static int _dragDropGroupIdx = -1; private static IModGroup? _dragDropGroup;
private static int _dragDropOptionIdx = -1; private static IModOption? _dragDropOption;
public static void Reset() public static void Reset()
{ {
_newOptionNameIdx = -1; _newOptionNameIdx = -1;
_newOptionName = string.Empty; _newOptionName = string.Empty;
_dragDropGroupIdx = -1; _dragDropGroup = null;
_dragDropOptionIdx = -1; _dragDropOption = null;
} }
public static void Draw(ModPanelEditTab panel, int groupIdx) public static void Draw(ModPanelEditTab panel, int groupIdx)
@ -491,6 +492,7 @@ public class ModPanelEditTab(
EditOption(panel, multi, groupIdx, optionIdx); EditOption(panel, multi, groupIdx, optionIdx);
break; break;
} }
DrawNewOption(panel, groupIdx, UiHelpers.IconButtonSize); DrawNewOption(panel, groupIdx, UiHelpers.IconButtonSize);
} }
@ -502,8 +504,8 @@ public class ModPanelEditTab(
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
ImGui.Selectable($"Option #{optionIdx + 1}"); ImGui.Selectable($"Option #{optionIdx + 1}");
Source(group, groupIdx, optionIdx); Source(option);
Target(panel, group, groupIdx, optionIdx); Target(panel, group, optionIdx);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
@ -511,7 +513,7 @@ public class ModPanelEditTab(
if (group.Type == GroupType.Single) if (group.Type == GroupType.Single)
{ {
if (ImGui.RadioButton("##default", group.DefaultSettings.AsIndex == optionIdx)) 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."); 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); var isDefaultOption = group.DefaultSettings.HasFlag(optionIdx);
if (ImGui.Checkbox("##default", ref isDefaultOption)) if (ImGui.Checkbox("##default", ref isDefaultOption))
panel._modManager.OptionEditor.ChangeModGroupDefaultOption(panel._mod, groupIdx, panel._modManager.OptionEditor.ChangeModGroupDefaultOption(group, group.DefaultSettings.SetBit(optionIdx, isDefaultOption));
group.DefaultSettings.SetBit(optionIdx, isDefaultOption));
ImGuiUtil.HoverTooltip($"{(isDefaultOption ? "Disable" : "Enable")} {option.Name} per default in this group."); ImGuiUtil.HoverTooltip($"{(isDefaultOption ? "Disable" : "Enable")} {option.Name} per default in this group.");
} }
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (Input.Text("##Name", groupIdx, optionIdx, option.Name, out var newOptionName, 256, -1)) 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(); ImGui.TableNextColumn();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Edit.ToIconString(), UiHelpers.IconButtonSize, "Edit option description.", if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Edit.ToIconString(), UiHelpers.IconButtonSize, "Edit option description.",
@ -537,15 +538,15 @@ public class ModPanelEditTab(
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize,
"Delete this option.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true)) "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(); ImGui.TableNextColumn();
if (group is not MultiModGroup multi) if (option is not MultiSubMod multi)
return; 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)) 50 * UiHelpers.Scale))
panel._modManager.OptionEditor.ChangeOptionPriority(panel._mod, groupIdx, optionIdx, priority); panel._modManager.OptionEditor.MultiEditor.ChangeOptionPriority(multi, priority);
ImGuiUtil.HoverTooltip("Option priority."); ImGuiUtil.HoverTooltip("Option priority.");
} }
@ -564,7 +565,7 @@ public class ModPanelEditTab(
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
ImGui.Selectable($"Option #{count + 1}"); ImGui.Selectable($"Option #{count + 1}");
Target(panel, group, groupIdx, count); Target(panel, group, count);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.SetNextItemWidth(-1); ImGui.SetNextItemWidth(-1);
@ -585,14 +586,14 @@ public class ModPanelEditTab(
tt, !(canAddGroup && validName), true)) tt, !(canAddGroup && validName), true))
return; return;
panel._modManager.OptionEditor.AddOption(mod, groupIdx, _newOptionName); panel._modManager.OptionEditor.AddOption(group, _newOptionName);
_newOptionName = string.Empty; _newOptionName = string.Empty;
} }
// Handle drag and drop to move options inside a group or into another group. // 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; return;
using var source = ImRaii.DragDropSource(); using var source = ImRaii.DragDropSource();
@ -601,14 +602,14 @@ public class ModPanelEditTab(
if (ImGui.SetDragDropPayload(DragDropLabel, IntPtr.Zero, 0)) if (ImGui.SetDragDropPayload(DragDropLabel, IntPtr.Zero, 0))
{ {
_dragDropGroupIdx = groupIdx; _dragDropGroup = option.Group;
_dragDropOptionIdx = optionIdx; _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) if (group is not ITexToolsGroup)
return; return;
@ -617,39 +618,53 @@ public class ModPanelEditTab(
if (!target.Success || !ImGuiUtil.IsDropping(DragDropLabel)) if (!target.Success || !ImGuiUtil.IsDropping(DragDropLabel))
return; 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._delayedActions.Enqueue(
() => panel._modManager.OptionEditor.MoveOption(panel._mod, groupIdx, sourceOption, optionIdx)); () => panel._modManager.OptionEditor.MoveOption(sourceOption, optionIdx));
} }
else else
{ {
// Move from one group to another by deleting, then adding, then moving the option. // Move from one group to another by deleting, then adding, then moving the option.
var sourceGroupIdx = _dragDropGroupIdx; var sourceOption = _dragDropOption;
var sourceOption = _dragDropOptionIdx;
var sourceGroup = panel._mod.Groups[sourceGroupIdx];
var currentCount = group.DataContainers.Count;
var option = ((ITexToolsGroup) sourceGroup).OptionData[_dragDropOptionIdx];
panel._delayedActions.Enqueue(() => panel._delayedActions.Enqueue(() =>
{ {
panel._modManager.OptionEditor.DeleteOption(panel._mod, sourceGroupIdx, sourceOption); panel._modManager.OptionEditor.DeleteOption(sourceOption);
panel._modManager.OptionEditor.AddOption(panel._mod, groupIdx, option); if (panel._modManager.OptionEditor.AddOption(group, sourceOption) is { } newOption)
panel._modManager.OptionEditor.MoveOption(panel._mod, groupIdx, currentCount, optionIdx); panel._modManager.OptionEditor.MoveOption(newOption, optionIdx);
}); });
} }
} }
_dragDropGroupIdx = -1; _dragDropGroup = null;
_dragDropOptionIdx = -1; _dragDropOption = null;
} }
} }
/// <summary> Draw a combo to select single or multi group and switch between them. </summary> /// <summary> Draw a combo to select single or multi group and switch between them. </summary>
private void DrawGroupCombo(IModGroup group, int groupIdx) 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) static string GroupTypeName(GroupType type)
=> type switch => type switch
{ {
@ -657,23 +672,6 @@ public class ModPanelEditTab(
GroupType.Multi => "Multi Group", GroupType.Multi => "Multi Group",
_ => "Unknown", _ => "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.");
} }
/// <summary> Handles input text and integers in separate fields without buffers for every single one. </summary> /// <summary> Handles input text and integers in separate fields without buffers for every single one. </summary>