Start for combining groups.

This commit is contained in:
Ottermandias 2025-01-10 15:42:23 +01:00
parent 9559bd7358
commit e77fa18c61
9 changed files with 468 additions and 26 deletions

View file

@ -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;
/// <summary> Groups that allow all available options to be selected at once. </summary>
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<CombiningSubMod> OptionData = [];
public List<CombinedDataContainer> Data { get; private set; }
/// <summary> Groups that allow all available options to be selected at once. </summary>
public CombiningModGroup(Mod mod)
{
Mod = mod;
Data = [new CombinedDataContainer(this)];
}
IReadOnlyList<IModOption> IModGroup.Options
=> OptionData;
public IReadOnlyList<IModDataContainer> 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<CombinedDataContainer>(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<CombinedDataContainer>(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<Utf8GamePath, FullPath> redirections, MetaDictionary manipulations)
=> Data[setting.AsIndex].AddDataTo(redirections, manipulations);
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData?> 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)));
/// <summary> Create a group without a mod only for saving it in the creator. </summary>
internal static CombiningModGroup WithoutMod(string name)
=> new(null!)
{
Name = name,
};
/// <summary> For loading when no empty container should be created. </summary>
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)));
}
}

View file

@ -23,6 +23,7 @@ public enum GroupDrawBehaviour
public interface IModGroup
{
public const int MaxMultiOptions = 32;
public const int MaxCombiningOptions = 8;
public Mod Mod { get; }
public string Name { get; set; }

View file

@ -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<CombiningModGroup, CombiningSubMod>(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;
}
}

View file

@ -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;
/// <summary> Change the settings stored as default options in a mod.</summary>
public void ChangeModGroupDefaultOption(IModGroup group, Setting defaultOption)
{
@ -223,6 +227,9 @@ public class ModGroupEditor(
case ImcSubMod i:
ImcEditor.DeleteOption(i);
return;
case CombiningModGroup c:
CombiningEditor.DeleteOption(c);
return;
}
}
@ -232,6 +239,7 @@ public class ModGroupEditor(
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,
};
@ -241,6 +249,7 @@ public class ModGroupEditor(
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,
};
@ -250,6 +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),
_ => null,
};
@ -259,6 +269,7 @@ public class ModGroupEditor(
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),
};
@ -268,6 +279,7 @@ public class ModGroupEditor(
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),
};
@ -284,6 +296,9 @@ public class ModGroupEditor(
case ImcSubMod i:
ImcEditor.MoveOption(i, toIdx);
return;
case CombiningSubMod c:
CombiningEditor.MoveOption(c, toIdx);
return;
}
}
}

View file

@ -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<Utf8GamePath, FullPath> Files { get; set; } = [];
public Dictionary<Utf8GamePath, FullPath> FileSwaps { get; set; } = [];
public MetaDictionary Manipulations { get; set; } = new();
public void AddDataTo(Dictionary<Utf8GamePath, FullPath> 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>() ?? string.Empty;
}
}

View file

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

View file

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

View file

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

View file

@ -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()
{
}
}