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