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