diff --git a/OtterGui b/OtterGui
index fd387218..055f1695 160000
--- a/OtterGui
+++ b/OtterGui
@@ -1 +1 @@
-Subproject commit fd387218d2d2d237075cb35be6ca89eeb53e14e5
+Subproject commit 055f169572223fd1b59389549c88b4c861c94608
diff --git a/Penumbra/Mods/Groups/CombiningModGroup.cs b/Penumbra/Mods/Groups/CombiningModGroup.cs
new file mode 100644
index 00000000..80f3c4c0
--- /dev/null
+++ b/Penumbra/Mods/Groups/CombiningModGroup.cs
@@ -0,0 +1,196 @@
+using Dalamud.Interface.ImGuiNotification;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using OtterGui;
+using OtterGui.Classes;
+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 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,
+ };
+ return OptionData.AddNewWithPowerSet(Data, subMod, () => new CombinedDataContainer(this), IModGroup.MaxCombiningOptions)
+ ? subMod
+ : null;
+ }
+
+ 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.Data.EnsureCapacity(requiredContainers);
+ ret.Data.AddRange(Enumerable.Repeat(0, requiredContainers - ret.Data.Count).Select(_ => new CombinedDataContainer(ret)));
+ }
+
+ 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 = [];
+ }
+}
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..ce5db454
--- /dev/null
+++ b/Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs
@@ -0,0 +1,48 @@
+using OtterGui;
+using OtterGui.Classes;
+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)
+ => throw new NotImplementedException();
+
+ protected override void RemoveOption(CombiningModGroup group, int optionIndex)
+ {
+ if (group.OptionData.RemoveWithPowerSet(group.Data, optionIndex))
+ group.DefaultSettings.RemoveBit(optionIndex);
+ }
+
+ protected override bool MoveOption(CombiningModGroup group, int optionIdxFrom, int optionIdxTo)
+ {
+ if (!group.OptionData.MoveWithPowerSet(group.Data, ref optionIdxFrom, ref optionIdxTo))
+ return false;
+
+ group.DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo);
+ return true;
+ }
+
+ public void SetDisplayName(CombinedDataContainer container, string name, SaveType saveType = SaveType.Queue)
+ {
+ if (container.Name == name)
+ return;
+
+ container.Name = name;
+ SaveService.Save(saveType, new ModSaveGroup(container.Group, Config.ReplaceNonAsciiOnImport));
+ Communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, container.Group.Mod, container.Group, null, null, -1);
+ }
+}
diff --git a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs
index d01297db..1c077c58 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 CombiningSubMod 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, 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/ModCreator.cs b/Penumbra/Mods/ModCreator.cs
index bdc16b72..18d2bc09 100644
--- a/Penumbra/Mods/ModCreator.cs
+++ b/Penumbra/Mods/ModCreator.cs
@@ -82,13 +82,11 @@ public partial class ModCreator(
if (incorporateMetaChanges)
IncorporateAllMetaChanges(mod, true);
if (deleteDefaultMetaChanges && !Config.KeepDefaultMetaChanges)
- {
foreach (var container in mod.AllDataContainers)
{
if (ModMetaEditor.DeleteDefaultValues(metaFileManager, container.Manipulations))
saveService.ImmediateSaveSync(new ModSaveGroup(container, Config.ReplaceNonAsciiOnImport));
}
- }
return true;
}
@@ -186,7 +184,8 @@ public partial class ModCreator(
/// If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod.
/// If delete is true, the files are deleted afterwards.
///
- public (bool Changes, List DeleteList) IncorporateMetaChanges(IModDataContainer option, DirectoryInfo basePath, bool delete, bool deleteDefault)
+ public (bool Changes, List DeleteList) IncorporateMetaChanges(IModDataContainer option, DirectoryInfo basePath, bool delete,
+ bool deleteDefault)
{
var deleteList = new List();
var oldSize = option.Manipulations.Count;
@@ -447,9 +446,10 @@ public partial class ModCreator(
var json = JObject.Parse(File.ReadAllText(file.FullName));
switch (json[nameof(Type)]?.ToObject() ?? GroupType.Single)
{
- case GroupType.Multi: return MultiModGroup.Load(mod, json);
- case GroupType.Single: return SingleModGroup.Load(mod, json);
- case GroupType.Imc: return ImcModGroup.Load(mod, json);
+ case GroupType.Multi: return MultiModGroup.Load(mod, json);
+ case GroupType.Single: return SingleModGroup.Load(mod, json);
+ case GroupType.Imc: return ImcModGroup.Load(mod, json);
+ case GroupType.Combining: return CombiningModGroup.Load(mod, json);
}
}
catch (Exception e)
diff --git a/Penumbra/Mods/SubMods/CombinedDataContainer.cs b/Penumbra/Mods/SubMods/CombinedDataContainer.cs
new file mode 100644
index 00000000..2c410c1c
--- /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;
+
+namespace Penumbra.Mods.SubMods;
+
+public class CombinedDataContainer(IModGroup group) : IModDataContainer
+{
+ public IMod Mod
+ => Group.Mod;
+
+ public IModGroup Group { get; } = group;
+
+ public string Name { get; set; } = 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)
+ {
+ 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/Mods/SubMods/SubMod.cs b/Penumbra/Mods/SubMods/SubMod.cs
index f6b1be96..7f01884d 100644
--- a/Penumbra/Mods/SubMods/SubMod.cs
+++ b/Penumbra/Mods/SubMods/SubMod.cs
@@ -81,29 +81,40 @@ public static class SubMod
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
public static void WriteModContainer(JsonWriter j, JsonSerializer serializer, IModDataContainer data, DirectoryInfo basePath)
{
- j.WritePropertyName(nameof(data.Files));
- j.WriteStartObject();
- foreach (var (gamePath, file) in data.Files)
+ if (data.Files.Count > 0)
{
- if (file.ToRelPath(basePath, out var relPath))
+ j.WritePropertyName(nameof(data.Files));
+ j.WriteStartObject();
+ foreach (var (gamePath, file) in data.Files)
+ {
+ if (file.ToRelPath(basePath, out var relPath))
+ {
+ j.WritePropertyName(gamePath.ToString());
+ j.WriteValue(relPath.ToString());
+ }
+ }
+
+ j.WriteEndObject();
+ }
+
+ if (data.FileSwaps.Count > 0)
+ {
+ j.WritePropertyName(nameof(data.FileSwaps));
+ j.WriteStartObject();
+ foreach (var (gamePath, file) in data.FileSwaps)
{
j.WritePropertyName(gamePath.ToString());
- j.WriteValue(relPath.ToString());
+ j.WriteValue(file.ToString());
}
+
+ j.WriteEndObject();
}
- j.WriteEndObject();
- j.WritePropertyName(nameof(data.FileSwaps));
- j.WriteStartObject();
- foreach (var (gamePath, file) in data.FileSwaps)
+ if (data.Manipulations.Count > 0)
{
- j.WritePropertyName(gamePath.ToString());
- j.WriteValue(file.ToString());
+ j.WritePropertyName(nameof(data.Manipulations));
+ serializer.Serialize(j, data.Manipulations);
}
-
- j.WriteEndObject();
- j.WritePropertyName(nameof(data.Manipulations));
- serializer.Serialize(j, data.Manipulations);
}
/// Write the data for a selectable mod option on a JsonWriter.
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..f32e6da6
--- /dev/null
+++ b/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs
@@ -0,0 +1,111 @@
+using Dalamud.Interface;
+using ImGuiNET;
+using OtterGui;
+using OtterGui.Raii;
+using OtterGui.Text;
+using Penumbra.Mods.Groups;
+using Penumbra.Mods.SubMods;
+
+namespace Penumbra.UI.ModsTab.Groups;
+
+public readonly struct CombiningModGroupEditDrawer(ModGroupEditDrawer editor, CombiningModGroup group) : IModGroupEditDrawer
+{
+ public void Draw()
+ {
+ foreach (var (option, optionIdx) in group.OptionData.WithIndex())
+ {
+ using var id = ImUtf8.PushId(optionIdx);
+ editor.DrawOptionPosition(group, option, optionIdx);
+
+ ImUtf8.SameLineInner();
+ editor.DrawOptionDefaultMultiBehaviour(group, option, optionIdx);
+
+ ImUtf8.SameLineInner();
+ editor.DrawOptionName(option);
+
+ ImUtf8.SameLineInner();
+ editor.DrawOptionDescription(option);
+
+ ImUtf8.SameLineInner();
+ editor.DrawOptionDelete(option);
+ }
+
+ DrawNewOption();
+ DrawContainerNames();
+ }
+
+ private void DrawNewOption()
+ {
+ var count = group.OptionData.Count;
+ if (count >= IModGroup.MaxCombiningOptions)
+ return;
+
+ var name = editor.DrawNewOptionBase(group, count);
+
+ var validName = name.Length > 0;
+ if (ImUtf8.IconButton(FontAwesomeIcon.Plus, validName
+ ? "Add a new option to this group."u8
+ : "Please enter a name for the new option."u8, default, !validName))
+ {
+ editor.ModManager.OptionEditor.CombiningEditor.AddOption(group, name);
+ editor.NewOptionName = null;
+ }
+ }
+
+ private unsafe void DrawContainerNames()
+ {
+ if (ImUtf8.ButtonEx("Edit Container Names"u8,
+ "Add optional names to separate data containers of the combining group.\nThose are just for easier identification while editing the mod, and are not generally displayed to the user."u8,
+ new Vector2(400 * ImUtf8.GlobalScale, 0)))
+ ImUtf8.OpenPopup("DataContainerNames"u8);
+
+ var sizeX = group.OptionData.Count * (ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetFrameHeight()) + 300 * ImUtf8.GlobalScale;
+ ImGui.SetNextWindowSize(new Vector2(sizeX, ImGui.GetFrameHeightWithSpacing() * Math.Min(16, group.Data.Count) + 200 * ImUtf8.GlobalScale));
+ using var popup = ImUtf8.Popup("DataContainerNames"u8);
+ if (!popup)
+ return;
+
+ foreach (var option in group.OptionData)
+ {
+ ImUtf8.RotatedText(option.Name, true);
+ ImUtf8.SameLineInner();
+ }
+
+ ImGui.NewLine();
+ ImGui.Separator();
+ using var child = ImUtf8.Child("##Child"u8, ImGui.GetContentRegionAvail());
+ ImGuiClip.ClippedDraw(group.Data, DrawRow, ImGui.GetFrameHeightWithSpacing());
+ }
+
+ private void DrawRow(CombinedDataContainer container, int index)
+ {
+ using var id = ImUtf8.PushId(index);
+ using (ImRaii.Disabled())
+ {
+ for (var i = 0; i < group.OptionData.Count; ++i)
+ {
+ id.Push(i);
+ var check = (index & (1 << i)) != 0;
+ ImUtf8.Checkbox(""u8, ref check);
+ ImUtf8.SameLineInner();
+ id.Pop();
+ }
+ }
+
+ var name = editor.CombiningDisplayIndex == index ? editor.CombiningDisplayName ?? container.Name : container.Name;
+ if (ImUtf8.InputText("##Nothing"u8, ref name, "Optional Display Name..."u8))
+ {
+ editor.CombiningDisplayIndex = index;
+ editor.CombiningDisplayName = name;
+ }
+
+ if (ImGui.IsItemDeactivatedAfterEdit())
+ editor.ModManager.OptionEditor.CombiningEditor.SetDisplayName(container, name);
+
+ if (ImGui.IsItemDeactivated())
+ {
+ editor.CombiningDisplayIndex = -1;
+ editor.CombiningDisplayName = null;
+ }
+ }
+}
diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs
index ec5bb920..89812346 100644
--- a/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs
+++ b/Penumbra/UI/ModsTab/Groups/ModGroupEditDrawer.cs
@@ -58,6 +58,9 @@ public sealed class ModGroupEditDrawer(
private IModOption? _dragDropOption;
private bool _draggingAcross;
+ internal string? CombiningDisplayName;
+ internal int CombiningDisplayIndex;
+
public void Draw(Mod mod)
{
PrepareStyle();
@@ -275,6 +278,7 @@ public sealed class ModGroupEditDrawer(
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal string DrawNewOptionBase(IModGroup group, int count)
{
+ ImGui.AlignTextToFramePadding();
ImUtf8.Selectable($"Option #{count + 1}", false, size: OptionIdxSelectable);
Target(group, count);