diff --git a/Penumbra/Mods/Groups/CombiningModGroup.cs b/Penumbra/Mods/Groups/CombiningModGroup.cs
new file mode 100644
index 00000000..255f84aa
--- /dev/null
+++ b/Penumbra/Mods/Groups/CombiningModGroup.cs
@@ -0,0 +1,235 @@
+using Dalamud.Interface.ImGuiNotification;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using OtterGui;
+using OtterGui.Classes;
+using OtterGui.Filesystem;
+using Penumbra.Api.Enums;
+using Penumbra.GameData.Data;
+using Penumbra.Meta.Manipulations;
+using Penumbra.Mods.Settings;
+using Penumbra.Mods.SubMods;
+using Penumbra.String.Classes;
+using Penumbra.UI.ModsTab.Groups;
+using Penumbra.Util;
+
+namespace Penumbra.Mods.Groups;
+
+/// Groups that allow all available options to be selected at once.
+public sealed class CombiningModGroup : IModGroup
+{
+
+ public GroupType Type
+ => GroupType.Combining;
+
+ public GroupDrawBehaviour Behaviour
+ => GroupDrawBehaviour.MultiSelection;
+
+ public Mod Mod { get; }
+ public string Name { get; set; } = "Group";
+ public string Description { get; set; } = string.Empty;
+ public string Image { get; set; } = string.Empty;
+ public ModPriority Priority { get; set; }
+ public int Page { get; set; }
+ public Setting DefaultSettings { get; set; }
+ public readonly List OptionData = [];
+ public List Data { get; private set; }
+
+ /// Groups that allow all available options to be selected at once.
+ public CombiningModGroup(Mod mod)
+ {
+ Mod = mod;
+ Data = [new CombinedDataContainer(this)];
+ }
+
+ IReadOnlyList IModGroup.Options
+ => OptionData;
+
+ public IReadOnlyList DataContainers
+ => Data;
+
+ public bool IsOption
+ => OptionData.Count > 0;
+
+ public FullPath? FindBestMatch(Utf8GamePath gamePath)
+ {
+ foreach (var path in Data.SelectWhere(o
+ => (o.Files.TryGetValue(gamePath, out var file) || o.FileSwaps.TryGetValue(gamePath, out file), file)))
+ return path;
+
+ return null;
+ }
+
+ public void RemoveOption(int index)
+ {
+ if(index < 0 || index >= OptionData.Count)
+ return;
+
+ OptionData.RemoveAt(index);
+ var list = new List(Data.Count / 2);
+ var optionFlag = 1 << index;
+ list.AddRange(Data.Where((c, i) => (i & optionFlag) == 0));
+ Data = list;
+ }
+
+ public void MoveOption(int from, int to)
+ {
+ if (!OptionData.Move(ref from, ref to))
+ return;
+
+ var list = new List(Data.Count);
+ for (var i = 0ul; i < (ulong)Data.Count; ++i)
+ {
+ var actualIndex = (int) Functions.MoveBit(i, from, to);
+ list.Add(Data[actualIndex]);
+ }
+
+ Data = list;
+ }
+
+ public IModOption? AddOption(string name, string description = "")
+ {
+ var groupIdx = Mod.Groups.IndexOf(this);
+ if (groupIdx < 0)
+ return null;
+
+ var subMod = new CombiningSubMod(this)
+ {
+ Name = name,
+ Description = description,
+ };
+ // Double available containers.
+ FillContainers(2 * Data.Count);
+ OptionData.Add(subMod);
+ return subMod;
+ }
+
+ public static CombiningModGroup? Load(Mod mod, JObject json)
+ {
+ var ret = new CombiningModGroup(mod, true);
+ if (!ModSaveGroup.ReadJsonBase(json, ret))
+ return null;
+
+ var options = json["Options"];
+ if (options != null)
+ foreach (var child in options.Children())
+ {
+ if (ret.OptionData.Count == IModGroup.MaxCombiningOptions)
+ {
+ Penumbra.Messager.NotificationMessage(
+ $"Combining Group {ret.Name} in {mod.Name} has more than {IModGroup.MaxCombiningOptions} options, ignoring excessive options.",
+ NotificationType.Warning);
+ break;
+ }
+
+ var subMod = new CombiningSubMod(ret, child);
+ ret.OptionData.Add(subMod);
+ }
+
+ var requiredContainers = 1 << ret.OptionData.Count;
+ var containers = json["Containers"];
+ if (containers != null)
+ foreach (var child in containers.Children())
+ {
+ if (requiredContainers <= ret.Data.Count)
+ {
+ Penumbra.Messager.NotificationMessage(
+ $"Combining Group {ret.Name} in {mod.Name} has more data containers than it can support with {ret.OptionData.Count} options, ignoring excessive containers.",
+ NotificationType.Warning);
+ break;
+ }
+
+ var container = new CombinedDataContainer(ret, child);
+ ret.Data.Add(container);
+ }
+
+ if (requiredContainers > ret.Data.Count)
+ {
+ Penumbra.Messager.NotificationMessage(
+ $"Combining Group {ret.Name} in {mod.Name} has not enough data containers for its {ret.OptionData.Count} options, filling with empty containers.",
+ NotificationType.Warning);
+ ret.FillContainers(requiredContainers);
+ }
+
+ ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings);
+
+ return ret;
+ }
+
+ public int GetIndex()
+ => ModGroup.GetIndex(this);
+
+ public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer)
+ => new CombiningModGroupEditDrawer(editDrawer, this);
+
+ public void AddData(Setting setting, Dictionary redirections, MetaDictionary manipulations)
+ => Data[setting.AsIndex].AddDataTo(redirections, manipulations);
+
+ public void AddChangedItems(ObjectIdentification identifier, IDictionary changedItems)
+ {
+ foreach (var container in DataContainers)
+ identifier.AddChangedItems(container, changedItems);
+ }
+
+ public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null)
+ {
+ ModSaveGroup.WriteJsonBase(jWriter, this);
+ jWriter.WritePropertyName("Options");
+ jWriter.WriteStartArray();
+ foreach (var option in OptionData)
+ {
+ jWriter.WriteStartObject();
+ SubMod.WriteModOption(jWriter, option);
+ jWriter.WriteEndObject();
+ }
+
+ jWriter.WriteEndArray();
+
+ jWriter.WritePropertyName("Containers");
+ jWriter.WriteStartArray();
+ foreach (var container in Data)
+ {
+ jWriter.WriteStartObject();
+ if (container.Name.Length > 0)
+ {
+ jWriter.WritePropertyName("Name");
+ jWriter.WriteValue(container.Name);
+ }
+
+ SubMod.WriteModContainer(jWriter, serializer, container, basePath ?? Mod.ModPath);
+ jWriter.WriteEndObject();
+ }
+
+ jWriter.WriteEndArray();
+ }
+
+ public (int Redirections, int Swaps, int Manips) GetCounts()
+ => ModGroup.GetCountsBase(this);
+
+ public Setting FixSetting(Setting setting)
+ => new(Math.Min(setting.Value, (ulong)(Data.Count - 1)));
+
+ /// Create a group without a mod only for saving it in the creator.
+ internal static CombiningModGroup WithoutMod(string name)
+ => new(null!)
+ {
+ Name = name,
+ };
+
+ /// For loading when no empty container should be created.
+ private CombiningModGroup(Mod mod, bool _)
+ {
+ Mod = mod;
+ Data = [];
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
+ private void FillContainers(int requiredCount)
+ {
+ if (requiredCount <= Data.Count)
+ return;
+
+ Data.EnsureCapacity(requiredCount);
+ Data.AddRange(Enumerable.Repeat(0, requiredCount - Data.Count).Select(_ => new CombinedDataContainer(this)));
+ }
+}
diff --git a/Penumbra/Mods/Groups/IModGroup.cs b/Penumbra/Mods/Groups/IModGroup.cs
index a6f6e20d..96422caf 100644
--- a/Penumbra/Mods/Groups/IModGroup.cs
+++ b/Penumbra/Mods/Groups/IModGroup.cs
@@ -22,7 +22,8 @@ public enum GroupDrawBehaviour
public interface IModGroup
{
- public const int MaxMultiOptions = 32;
+ public const int MaxMultiOptions = 32;
+ public const int MaxCombiningOptions = 8;
public Mod Mod { get; }
public string Name { get; set; }
diff --git a/Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs
new file mode 100644
index 00000000..46c8e3db
--- /dev/null
+++ b/Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs
@@ -0,0 +1,72 @@
+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.OptionEditor;
+
+public sealed class CombiningModGroupEditor(CommunicatorService communicator, SaveService saveService, Configuration config)
+ : ModOptionEditor(communicator, saveService, config), IService
+{
+ protected override CombiningModGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync)
+ => new(mod)
+ {
+ Name = newName,
+ Priority = priority,
+ };
+
+ protected override CombiningSubMod? CloneOption(CombiningModGroup group, IModOption option)
+ {
+ if (group.OptionData.Count >= IModGroup.MaxCombiningOptions)
+ {
+ Penumbra.Log.Error(
+ $"Could not add option {option.Name} to {group.Name} for mod {group.Mod.Name}, "
+ + $"since only up to {IModGroup.MaxCombiningOptions} options are supported in one group.");
+ return null;
+ }
+
+ var newOption = new CombiningSubMod(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(CombiningModGroup group, int optionIndex)
+ {
+ var optionFlag = 1 << optionIndex;
+ for (var i = group.Data.Count - 1; i >= 0; --i)
+ {
+ group.Data.RemoveAll()
+ if ((i & optionFlag) == optionFlag)
+ group.Data.RemoveAt(i);
+ }
+
+ group.OptionData.RemoveAt(optionIndex);
+ group.DefaultSettings = group.DefaultSettings.RemoveBit(optionIndex);
+ }
+
+ protected override bool MoveOption(MultiModGroup group, int optionIdxFrom, int optionIdxTo)
+ {
+ if (!group.OptionData.Move(ref optionIdxFrom, ref optionIdxTo))
+ return false;
+
+ group.DefaultSettings = group.DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo);
+ return true;
+ }
+}
diff --git a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs
index d01297db..b66b4d8c 100644
--- a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs
+++ b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs
@@ -37,6 +37,7 @@ public class ModGroupEditor(
SingleModGroupEditor singleEditor,
MultiModGroupEditor multiEditor,
ImcModGroupEditor imcEditor,
+ CombiningModGroupEditor combiningEditor,
CommunicatorService communicator,
SaveService saveService,
Configuration config) : IService
@@ -50,6 +51,9 @@ public class ModGroupEditor(
public ImcModGroupEditor ImcEditor
=> imcEditor;
+ public CombiningModGroupEditor CombiningEditor
+ => combiningEditor;
+
/// Change the settings stored as default options in a mod.
public void ChangeModGroupDefaultOption(IModGroup group, Setting defaultOption)
{
@@ -223,52 +227,60 @@ public class ModGroupEditor(
case ImcSubMod i:
ImcEditor.DeleteOption(i);
return;
+ case CombiningModGroup c:
+ CombiningEditor.DeleteOption(c);
+ 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,
+ SingleModGroup s => SingleEditor.AddOption(s, option),
+ MultiModGroup m => MultiEditor.AddOption(m, option),
+ ImcModGroup i => ImcEditor.AddOption(i, option),
+ CombiningModGroup c => CombiningEditor.AddOption(c, 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,
+ SingleModGroup s => SingleEditor.AddOption(s, newName),
+ MultiModGroup m => MultiEditor.AddOption(m, newName),
+ ImcModGroup i => ImcEditor.AddOption(i, newName),
+ CombiningModGroup c => CombiningEditor.AddOption(c, 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, default, default, saveType),
- _ => null,
+ GroupType.Single => SingleEditor.AddModGroup(mod, newName, saveType),
+ GroupType.Multi => MultiEditor.AddModGroup(mod, newName, saveType),
+ GroupType.Imc => ImcEditor.AddModGroup(mod, newName, default, default, saveType),
+ GroupType.Combining => CombiningEditor.AddModGroup(mod, newName, default, default, 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),
+ GroupType.Single => SingleEditor.FindOrAddModGroup(mod, name, saveType),
+ GroupType.Multi => MultiEditor.FindOrAddModGroup(mod, name, saveType),
+ GroupType.Imc => ImcEditor.FindOrAddModGroup(mod, name, saveType),
+ GroupType.Combining => CombiningEditor.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),
+ SingleModGroup s => SingleEditor.FindOrAddOption(s, name, saveType),
+ MultiModGroup m => MultiEditor.FindOrAddOption(m, name, saveType),
+ ImcModGroup i => ImcEditor.FindOrAddOption(i, name, saveType),
+ CombiningModGroup c => CombiningEditor.FindOrAddOption(c, name, saveType),
+ _ => (null, -1, false),
};
public void MoveOption(IModOption option, int toIdx)
@@ -284,6 +296,9 @@ public class ModGroupEditor(
case ImcSubMod i:
ImcEditor.MoveOption(i, toIdx);
return;
+ case CombiningSubMod c:
+ CombiningEditor.MoveOption(c, toIdx);
+ return;
}
}
}
diff --git a/Penumbra/Mods/SubMods/CombinedDataContainer.cs b/Penumbra/Mods/SubMods/CombinedDataContainer.cs
new file mode 100644
index 00000000..3e8ec95b
--- /dev/null
+++ b/Penumbra/Mods/SubMods/CombinedDataContainer.cs
@@ -0,0 +1,72 @@
+using Newtonsoft.Json.Linq;
+using OtterGui;
+using Penumbra.Meta.Manipulations;
+using Penumbra.Mods.Editor;
+using Penumbra.Mods.Groups;
+using Penumbra.String.Classes;
+using Swan.Formatters;
+
+namespace Penumbra.Mods.SubMods;
+
+public class CombinedDataContainer(IModGroup group) : IModDataContainer
+{
+ public IMod Mod
+ => Group.Mod;
+
+ public IModGroup Group { get; } = group;
+
+ public string Name { get; } = string.Empty;
+ public Dictionary Files { get; set; } = [];
+ public Dictionary FileSwaps { get; set; } = [];
+ public MetaDictionary Manipulations { get; set; } = new();
+
+ public void AddDataTo(Dictionary redirections, MetaDictionary manipulations)
+ => SubMod.AddContainerTo(this, redirections, manipulations);
+
+ public string GetName()
+ {
+ if (Name.Length > 0)
+ return Name;
+
+ var index = GetDataIndex();
+ if (index == 0)
+ return "None";
+
+ var sb = new StringBuilder(128);
+ for (var i = 0; i < IModGroup.MaxCombiningOptions; ++i)
+ {
+ if ((index & 1) == 0)
+ continue;
+
+ sb.Append(Group.Options[i].Name);
+ sb.Append(' ').Append('+').Append(' ');
+ index >>= 1;
+ if (index == 0)
+ break;
+ }
+
+ return sb.ToString(0, sb.Length - 3);
+ }
+
+ public string GetFullName()
+ => $"{Group.Name}: {GetName()}";
+
+ public (int GroupIndex, int DataIndex) GetDataIndices()
+ => (Group.GetIndex(), GetDataIndex());
+
+ private int GetDataIndex()
+ {
+ var dataIndex = Group.DataContainers.IndexOf(this);
+ if (dataIndex < 0)
+ throw new Exception($"Group {Group.Name} from SubMod {Name} does not contain this SubMod.");
+
+ return dataIndex;
+ }
+
+ public CombinedDataContainer(CombiningModGroup group, JToken token)
+ : this(group)
+ {
+ SubMod.LoadDataContainer(token, this, group.Mod.ModPath);
+ Name = token["Name"]?.ToObject() ?? string.Empty;
+ }
+}
diff --git a/Penumbra/Mods/SubMods/CombiningSubMod.cs b/Penumbra/Mods/SubMods/CombiningSubMod.cs
new file mode 100644
index 00000000..6eb5de9d
--- /dev/null
+++ b/Penumbra/Mods/SubMods/CombiningSubMod.cs
@@ -0,0 +1,25 @@
+using Newtonsoft.Json.Linq;
+using Penumbra.Mods.Groups;
+
+namespace Penumbra.Mods.SubMods;
+
+public class CombiningSubMod(IModGroup group) : IModOption
+{
+ public IModGroup Group { get; } = group;
+
+ public Mod Mod
+ => Group.Mod;
+
+ public string Name { get; set; } = "Option";
+ public string Description { get; set; } = string.Empty;
+
+ public string FullName
+ => $"{Group.Name}: {Name}";
+
+ public int GetIndex()
+ => SubMod.GetIndex(this);
+
+ public CombiningSubMod(CombiningModGroup group, JToken json)
+ : this(group)
+ => SubMod.LoadOptionData(json, this);
+}
diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs
index 02e945f3..7f1a8ac5 100644
--- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs
+++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs
@@ -452,19 +452,17 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
private bool DrawOptionSelectHeader()
{
- const string defaultOption = "Default Option";
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero).Push(ImGuiStyleVar.FrameRounding, 0);
var width = new Vector2(ImGui.GetContentRegionAvail().X / 3, 0);
var ret = false;
- if (ImGuiUtil.DrawDisabledButton(defaultOption, width, "Switch to the default option for the mod.\nThis resets unsaved changes.",
- _editor.Option is DefaultSubMod))
+ if (ImUtf8.ButtonEx("Default Option"u8, "Switch to the default option for the mod.\nThis resets unsaved changes."u8, width, _editor.Option is DefaultSubMod))
{
_editor.LoadOption(-1, 0).Wait();
ret = true;
}
ImGui.SameLine();
- if (ImGuiUtil.DrawDisabledButton("Refresh Data", width, "Refresh data for the current option.\nThis resets unsaved changes.", false))
+ if (ImUtf8.ButtonEx("Refresh Data"u8, "Refresh data for the current option.\nThis resets unsaved changes."u8, width))
{
_editor.LoadMod(_editor.Mod!, _editor.GroupIdx, _editor.DataIdx).Wait();
ret = true;
@@ -474,7 +472,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
ImGui.SetNextItemWidth(width.X);
style.Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale);
using var color = ImRaii.PushColor(ImGuiCol.Border, ColorId.FolderLine.Value());
- using var combo = ImRaii.Combo("##optionSelector", _editor.Option!.GetFullName());
+ using var combo = ImUtf8.Combo("##optionSelector"u8, _editor.Option!.GetFullName());
if (!combo)
return ret;
diff --git a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs
index c30239bc..a3e7ce14 100644
--- a/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs
+++ b/Penumbra/UI/ModsTab/Groups/AddGroupDrawer.cs
@@ -48,6 +48,7 @@ public class AddGroupDrawer : IUiService
DrawSingleGroupButton(mod, buttonWidth);
ImUtf8.SameLineInner();
DrawMultiGroupButton(mod, buttonWidth);
+ DrawCombiningGroupButton(mod, buttonWidth);
}
private void DrawSingleGroupButton(Mod mod, Vector2 width)
@@ -76,6 +77,18 @@ public class AddGroupDrawer : IUiService
_groupNameValid = false;
}
+ private void DrawCombiningGroupButton(Mod mod, Vector2 width)
+ {
+ if (!ImUtf8.ButtonEx("Add Combining Group"u8, _groupNameValid
+ ? "Add a new combining option group to this mod."u8
+ : "Can not add a new group of this name."u8,
+ width, !_groupNameValid))
+ return;
+
+ _modManager.OptionEditor.AddModGroup(mod, GroupType.Combining, _groupName);
+ _groupName = string.Empty;
+ _groupNameValid = false;
+ }
private void DrawImcInput(float width)
{
var change = ImcMetaDrawer.DrawObjectType(ref _imcIdentifier, width);
diff --git a/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs
new file mode 100644
index 00000000..79d2fb43
--- /dev/null
+++ b/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs
@@ -0,0 +1,11 @@
+using Penumbra.Mods.Groups;
+
+namespace Penumbra.UI.ModsTab.Groups;
+
+public readonly struct CombiningModGroupEditDrawer(ModGroupEditDrawer editor, CombiningModGroup group) : IModGroupEditDrawer
+{
+ public void Draw()
+ {
+
+ }
+}