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
index 255f84aa..80f3c4c0 100644
--- a/Penumbra/Mods/Groups/CombiningModGroup.cs
+++ b/Penumbra/Mods/Groups/CombiningModGroup.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.GameData.Data;
using Penumbra.Meta.Manipulations;
@@ -18,7 +17,6 @@ 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;
@@ -60,33 +58,6 @@ public sealed class CombiningModGroup : IModGroup
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);
@@ -98,10 +69,9 @@ public sealed class CombiningModGroup : IModGroup
Name = name,
Description = description,
};
- // Double available containers.
- FillContainers(2 * Data.Count);
- OptionData.Add(subMod);
- return subMod;
+ return OptionData.AddNewWithPowerSet(Data, subMod, () => new CombinedDataContainer(this), IModGroup.MaxCombiningOptions)
+ ? subMod
+ : null;
}
public static CombiningModGroup? Load(Mod mod, JObject json)
@@ -148,7 +118,8 @@ public sealed class CombiningModGroup : IModGroup
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.Data.EnsureCapacity(requiredContainers);
+ ret.Data.AddRange(Enumerable.Repeat(0, requiredContainers - ret.Data.Count).Select(_ => new CombinedDataContainer(ret)));
}
ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings);
@@ -222,14 +193,4 @@ public sealed class CombiningModGroup : IModGroup
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/Manager/OptionEditor/CombiningModGroupEditor.cs b/Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs
index 46c8e3db..ce5db454 100644
--- a/Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs
+++ b/Penumbra/Mods/Manager/OptionEditor/CombiningModGroupEditor.cs
@@ -1,5 +1,5 @@
+using OtterGui;
using OtterGui.Classes;
-using OtterGui.Filesystem;
using OtterGui.Services;
using Penumbra.Mods.Groups;
using Penumbra.Mods.Settings;
@@ -19,54 +19,30 @@ public sealed class CombiningModGroupEditor(CommunicatorService communicator, Sa
};
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;
- }
+ => throw new NotImplementedException();
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);
+ if (group.OptionData.RemoveWithPowerSet(group.Data, optionIndex))
+ group.DefaultSettings.RemoveBit(optionIndex);
}
- protected override bool MoveOption(MultiModGroup group, int optionIdxFrom, int optionIdxTo)
+ protected override bool MoveOption(CombiningModGroup group, int optionIdxFrom, int optionIdxTo)
{
- if (!group.OptionData.Move(ref optionIdxFrom, ref optionIdxTo))
+ if (!group.OptionData.MoveWithPowerSet(group.Data, ref optionIdxFrom, ref optionIdxTo))
return false;
- group.DefaultSettings = group.DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo);
+ 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 b66b4d8c..1c077c58 100644
--- a/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs
+++ b/Penumbra/Mods/Manager/OptionEditor/ModGroupEditor.cs
@@ -227,7 +227,7 @@ public class ModGroupEditor(
case ImcSubMod i:
ImcEditor.DeleteOption(i);
return;
- case CombiningModGroup c:
+ case CombiningSubMod c:
CombiningEditor.DeleteOption(c);
return;
}
@@ -259,7 +259,7 @@ public class ModGroupEditor(
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),
+ GroupType.Combining => CombiningEditor.AddModGroup(mod, newName, saveType),
_ => null,
};
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
index 3e8ec95b..2c410c1c 100644
--- a/Penumbra/Mods/SubMods/CombinedDataContainer.cs
+++ b/Penumbra/Mods/SubMods/CombinedDataContainer.cs
@@ -4,7 +4,6 @@ using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor;
using Penumbra.Mods.Groups;
using Penumbra.String.Classes;
-using Swan.Formatters;
namespace Penumbra.Mods.SubMods;
@@ -15,7 +14,7 @@ public class CombinedDataContainer(IModGroup group) : IModDataContainer
public IModGroup Group { get; } = group;
- public string Name { get; } = string.Empty;
+ public string Name { get; set; } = string.Empty;
public Dictionary Files { get; set; } = [];
public Dictionary FileSwaps { get; set; } = [];
public MetaDictionary Manipulations { get; set; } = new();
@@ -35,11 +34,12 @@ public class CombinedDataContainer(IModGroup group) : IModDataContainer
var sb = new StringBuilder(128);
for (var i = 0; i < IModGroup.MaxCombiningOptions; ++i)
{
- if ((index & 1) == 0)
- continue;
+ if ((index & 1) != 0)
+ {
+ sb.Append(Group.Options[i].Name);
+ sb.Append(' ').Append('+').Append(' ');
+ }
- sb.Append(Group.Options[i].Name);
- sb.Append(' ').Append('+').Append(' ');
index >>= 1;
if (index == 0)
break;
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/ModsTab/Groups/CombiningModGroupEditDrawer.cs b/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs
index 79d2fb43..f32e6da6 100644
--- a/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs
+++ b/Penumbra/UI/ModsTab/Groups/CombiningModGroupEditDrawer.cs
@@ -1,4 +1,10 @@
+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;
@@ -6,6 +12,100 @@ public readonly struct CombiningModGroupEditDrawer(ModGroupEditDrawer editor, Co
{
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);