Now that was a lot of work.

This commit is contained in:
Ottermandias 2024-04-26 18:43:45 +02:00
parent 297be487b5
commit 1e5ed1c414
44 changed files with 1182 additions and 766 deletions

@ -1 +1 @@
Subproject commit 590629df33f9ad92baddd1d65ec8c986f18d608a
Subproject commit 69d106b457eb0f73d4b4caf1234da5631fd6fbf0

View file

@ -11,6 +11,7 @@ using Penumbra.Mods.Editor;
using Penumbra.Mods.Groups;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Settings;
using Penumbra.Mods.SubMods;
using Penumbra.Services;
namespace Penumbra.Api.Api;
@ -254,7 +255,7 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable
private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, Setting _1, int _2, bool inherited)
=> ModSettingChanged?.Invoke(type, collection.Id, mod?.ModPath.Name ?? string.Empty, inherited);
private void OnModOptionEdited(ModOptionChangeType type, Mod mod, int groupIndex, int optionIndex, int moveIndex)
private void OnModOptionEdited(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, int moveIndex)
{
switch (type)
{

View file

@ -7,8 +7,10 @@ using Penumbra.Communication;
using Penumbra.Interop.ResourceLoading;
using Penumbra.Meta;
using Penumbra.Mods;
using Penumbra.Mods.Groups;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Settings;
using Penumbra.Mods.SubMods;
using Penumbra.Services;
using Penumbra.String.Classes;
@ -257,7 +259,7 @@ public class CollectionCacheManager : IDisposable
}
/// <summary> Prepare Changes by removing mods from caches with collections or add or reload mods. </summary>
private void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx)
private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, int movedToIdx)
{
if (type is ModOptionChangeType.PrepareChange)
{

View file

@ -4,8 +4,10 @@ using OtterGui.Classes;
using Penumbra.Communication;
using Penumbra.Mods;
using Penumbra.Mods.Editor;
using Penumbra.Mods.Groups;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Settings;
using Penumbra.Mods.SubMods;
using Penumbra.Services;
namespace Penumbra.Collections.Manager;
@ -290,7 +292,7 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
}
/// <summary> Save all collections where the mod has settings and the change requires saving. </summary>
private void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx)
private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, int movedToIdx)
{
type.HandlingInfo(out var requiresSaving, out _, out _);
if (!requiresSaving)
@ -298,7 +300,7 @@ public class CollectionStorage : IReadOnlyList<ModCollection>, IDisposable
foreach (var collection in this)
{
if (collection.Settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false)
if (collection.Settings[mod.Index]?.HandleChanges(type, mod, group, option, movedToIdx) ?? false)
_saveService.QueueSave(new ModCollectionSave(_modStorage, collection));
}
}

View file

@ -1,8 +1,10 @@
using OtterGui.Classes;
using Penumbra.Api;
using Penumbra.Api.Api;
using Penumbra.Mods;
using Penumbra.Mods.Groups;
using Penumbra.Mods.Manager;
using Penumbra.Mods.SubMods;
using static Penumbra.Communication.ModOptionChanged;
namespace Penumbra.Communication;
@ -11,22 +13,23 @@ namespace Penumbra.Communication;
/// <list type="number">
/// <item>Parameter is the type option change. </item>
/// <item>Parameter is the changed mod. </item>
/// <item>Parameter is the index of the changed group inside the mod. </item>
/// <item>Parameter is the index of the changed option inside the group or -1 if it does not concern a specific option. </item>
/// <item>Parameter is the index of the group an option was moved to. </item>
/// <item>Parameter is the changed group inside the mod. </item>
/// <item>Parameter is the changed option inside the group or null if it does not concern a specific option. </item>
/// <item>Parameter is the changed data container inside the group or null if it does not concern a specific data container. </item>
/// <item>Parameter is the index of the group or option moved or deleted from. </item>
/// </list> </summary>
public sealed class ModOptionChanged()
: EventWrapper<ModOptionChangeType, Mod, int, int, int, ModOptionChanged.Priority>(nameof(ModOptionChanged))
: EventWrapper<ModOptionChangeType, Mod, IModGroup?, IModOption?, IModDataContainer?, int, Priority>(nameof(ModOptionChanged))
{
public enum Priority
{
/// <seealso cref="PenumbraApi.OnModOptionEdited"/>
/// <seealso cref="ModSettingsApi.OnModOptionEdited"/>
Api = int.MinValue,
/// <seealso cref="Collections.Cache.CollectionCacheManager.OnModOptionChange"/>
CollectionCacheManager = -100,
/// <seealso cref="Mods.Manager.ModCacheManager.OnModOptionChange"/>
/// <seealso cref="ModCacheManager.OnModOptionChange"/>
ModCacheManager = 0,
/// <seealso cref="UI.AdvancedWindow.ItemSwapTab.OnModOptionChange"/>

View file

@ -205,7 +205,7 @@ public partial class TexToolsImporter
{
var option = group.OptionList[idx];
_currentOptionName = option.Name;
options.Insert(idx, MultiSubMod.CreateForSaving(option.Name, option.Description, ModPriority.Default));
options.Insert(idx, MultiSubMod.WithoutGroup(option.Name, option.Description, ModPriority.Default));
if (option.IsChecked)
defaultSettings = Setting.Single(idx);
}

View file

@ -60,7 +60,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co
private void HandleDuplicate(Mod mod, FullPath duplicate, FullPath remaining, bool useModManager)
{
ModEditor.ApplyToAllOptions(mod, HandleSubMod);
ModEditor.ApplyToAllContainers(mod, HandleSubMod);
try
{
@ -73,7 +73,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co
return;
void HandleSubMod(IModDataContainer subMod, int groupIdx, int optionIdx)
void HandleSubMod(IModDataContainer subMod)
{
var changes = false;
var dict = subMod.Files.ToDictionary(kvp => kvp.Key,
@ -82,14 +82,9 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co
return;
if (useModManager)
{
modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, dict, SaveType.ImmediateSync);
}
modManager.OptionEditor.SetFiles(subMod, dict, SaveType.ImmediateSync);
else
{
subMod.Files = dict;
saveService.ImmediateSaveSync(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
}
saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, subMod, config.ReplaceNonAsciiOnImport));
}
}

View file

@ -29,8 +29,8 @@ public class ModEditor(
public int GroupIdx { get; private set; }
public int DataIdx { get; private set; }
public IModGroup? Group { get; private set; }
public IModDataContainer? Option { get; private set; }
public IModGroup? Group { get; private set; }
public IModDataContainer? Option { get; private set; }
public void LoadMod(Mod mod)
=> LoadMod(mod, -1, 0);
@ -63,10 +63,10 @@ public class ModEditor(
{
if (groupIdx == -1 && dataIdx == 0)
{
Group = null;
Option = Mod.Default;
GroupIdx = groupIdx;
DataIdx = dataIdx;
Group = null;
Option = Mod.Default;
GroupIdx = groupIdx;
DataIdx = dataIdx;
return;
}
@ -75,18 +75,18 @@ public class ModEditor(
Group = Mod.Groups[groupIdx];
if (dataIdx >= 0 && dataIdx < Group.DataContainers.Count)
{
Option = Group.DataContainers[dataIdx];
GroupIdx = groupIdx;
DataIdx = dataIdx;
Option = Group.DataContainers[dataIdx];
GroupIdx = groupIdx;
DataIdx = dataIdx;
return;
}
}
}
Group = null;
Option = Mod?.Default;
GroupIdx = -1;
DataIdx = 0;
Group = null;
Option = Mod?.Default;
GroupIdx = -1;
DataIdx = 0;
if (message)
Penumbra.Log.Error($"Loading invalid option {groupIdx} {dataIdx} for Mod {Mod?.Name ?? "Unknown"}.");
}
@ -105,23 +105,11 @@ public class ModEditor(
=> Clear();
/// <summary> Apply a option action to all available option in a mod, including the default option. </summary>
public static void ApplyToAllOptions(Mod mod, Action<IModDataContainer, int, int> action)
public static void ApplyToAllContainers(Mod mod, Action<IModDataContainer> action)
{
action(mod.Default, -1, 0);
foreach (var (group, groupIdx) in mod.Groups.WithIndex())
{
switch (group)
{
case SingleModGroup single:
for (var optionIdx = 0; optionIdx < single.OptionData.Count; ++optionIdx)
action(single.OptionData[optionIdx], groupIdx, optionIdx);
break;
case MultiModGroup multi:
for (var optionIdx = 0; optionIdx < multi.OptionData.Count; ++optionIdx)
action(multi.OptionData[optionIdx], groupIdx, optionIdx);
break;
}
}
action(mod.Default);
foreach (var container in mod.Groups.SelectMany(g => g.DataContainers))
action(container);
}
// Does not delete the base directory itself even if it is completely empty at the end.

View file

@ -24,8 +24,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu
num += dict.TryAdd(path.Item2, file.File) ? 0 : 1;
}
var (groupIdx, dataIdx) = option.GetDataIndices();
modManager.OptionEditor.OptionSetFiles(mod, groupIdx, dataIdx, dict);
modManager.OptionEditor.SetFiles(option, dict);
files.UpdatePaths(mod, option);
Changes = false;
return num;
@ -40,15 +39,15 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu
/// <summary> Remove all path redirections where the pointed-to file does not exist. </summary>
public void RemoveMissingPaths(Mod mod, IModDataContainer option)
{
void HandleSubMod(IModDataContainer subMod, int groupIdx, int optionIdx)
void HandleSubMod(IModDataContainer subMod)
{
var newDict = subMod.Files.Where(kvp => CheckAgainstMissing(mod, subMod, kvp.Value, kvp.Key, subMod == option))
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
if (newDict.Count != subMod.Files.Count)
modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, newDict);
modManager.OptionEditor.SetFiles(subMod, newDict);
}
ModEditor.ApplyToAllOptions(mod, HandleSubMod);
ModEditor.ApplyToAllContainers(mod, HandleSubMod);
files.ClearMissingFiles();
}

View file

@ -16,7 +16,7 @@ public class ModMerger : IDisposable
{
private readonly Configuration _config;
private readonly CommunicatorService _communicator;
private readonly ModOptionEditor _editor;
private readonly ModGroupEditor _editor;
private readonly ModFileSystemSelector _selector;
private readonly DuplicateManager _duplicates;
private readonly ModManager _mods;
@ -32,14 +32,14 @@ public class ModMerger : IDisposable
private readonly Dictionary<string, string> _fileToFile = [];
private readonly HashSet<string> _createdDirectories = [];
private readonly HashSet<int> _createdGroups = [];
private readonly HashSet<IModDataOption> _createdOptions = [];
private readonly HashSet<IModOption> _createdOptions = [];
public readonly HashSet<IModDataContainer> SelectedOptions = [];
public readonly IReadOnlyList<string> Warnings = [];
public Exception? Error { get; private set; }
public ModMerger(ModManager mods, ModOptionEditor editor, ModFileSystemSelector selector, DuplicateManager duplicates,
public ModMerger(ModManager mods, ModGroupEditor editor, ModFileSystemSelector selector, DuplicateManager duplicates,
CommunicatorService communicator, ModCreator creator, Configuration config)
{
_editor = editor;
@ -100,22 +100,23 @@ public class ModMerger : IDisposable
var (group, groupIdx, groupCreated) = _editor.FindOrAddModGroup(MergeToMod!, originalGroup.Type, originalGroup.Name);
if (groupCreated)
_createdGroups.Add(groupIdx);
if (group.Type != originalGroup.Type)
((List<string>)Warnings).Add(
$"The merged group {group.Name} already existed, but has a different type {group.Type} than the original group of type {originalGroup.Type}.");
if (group == null)
throw new Exception(
$"The merged group {originalGroup.Name} already existed, but had a different type than the original group of type {originalGroup.Type}.");
foreach (var originalOption in group.DataContainers)
{
var (option, _, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, originalOption.GetName());
var (option, _, optionCreated) = _editor.FindOrAddOption(group, originalOption.GetName());
if (optionCreated)
{
_createdOptions.Add((IModDataOption)option);
MergeIntoOption([originalOption], (IModDataOption)option, false);
_createdOptions.Add(option!);
// #TODO DataContainer <> Option.
MergeIntoOption([originalOption], (IModDataContainer)option!, false);
}
else
{
throw new Exception(
$"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}: The option {option.FullName} already existed.");
$"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}: The option {option!.FullName} already existed.");
}
}
}
@ -138,9 +139,9 @@ public class ModMerger : IDisposable
var (group, groupIdx, groupCreated) = _editor.FindOrAddModGroup(MergeToMod!, GroupType.Multi, groupName, SaveType.None);
if (groupCreated)
_createdGroups.Add(groupIdx);
var (option, _, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, optionName, SaveType.None);
var (option, _, optionCreated) = _editor.FindOrAddOption(group!, optionName, SaveType.None);
if (optionCreated)
_createdOptions.Add((IModDataOption)option);
_createdOptions.Add(option!);
var dir = ModCreator.NewOptionDirectory(MergeToMod!.ModPath, groupName, _config.ReplaceNonAsciiOnImport);
if (!dir.Exists)
_createdDirectories.Add(dir.FullName);
@ -148,7 +149,8 @@ public class ModMerger : IDisposable
if (!dir.Exists)
_createdDirectories.Add(dir.FullName);
CopyFiles(dir);
MergeIntoOption(MergeFromMod!.AllDataContainers.Reverse(), (IModDataOption)option, true);
// #TODO DataContainer <> Option.
MergeIntoOption(MergeFromMod!.AllDataContainers.Reverse(), (IModDataContainer)option!, true);
}
private void MergeIntoOption(IEnumerable<IModDataContainer> mergeOptions, IModDataContainer option, bool fromFileToFile)
@ -184,10 +186,9 @@ public class ModMerger : IDisposable
}
}
var (groupIdx, dataIdx) = option.GetDataIndices();
_editor.OptionSetFiles(MergeToMod!, groupIdx, dataIdx, redirections, SaveType.None);
_editor.OptionSetFileSwaps(MergeToMod!, groupIdx, dataIdx, swaps, SaveType.None);
_editor.OptionSetManipulations(MergeToMod!, groupIdx, dataIdx, manips, SaveType.ImmediateSync);
_editor.SetFiles(option, redirections, SaveType.None);
_editor.SetFileSwaps(option, swaps, SaveType.None);
_editor.SetManipulations(option, manips, SaveType.ImmediateSync);
return;
bool GetFullPath(FullPath input, out FullPath ret)
@ -261,30 +262,31 @@ public class ModMerger : IDisposable
if (mods.Count == 1)
{
var files = CopySubModFiles(mods[0], dir);
_editor.OptionSetFiles(result, -1, 0, files);
_editor.OptionSetFileSwaps(result, -1, 0, mods[0].FileSwaps);
_editor.OptionSetManipulations(result, -1, 0, mods[0].Manipulations);
_editor.SetFiles(result.Default, files);
_editor.SetFileSwaps(result.Default, mods[0].FileSwaps);
_editor.SetManipulations(result.Default, mods[0].Manipulations);
}
else
{
foreach (var originalOption in mods)
{
if (originalOption.Group is not {} originalGroup)
if (originalOption.Group is not { } originalGroup)
{
var files = CopySubModFiles(mods[0], dir);
_editor.OptionSetFiles(result, -1, 0, files);
_editor.OptionSetFileSwaps(result, -1, 0, mods[0].FileSwaps);
_editor.OptionSetManipulations(result, -1, 0, mods[0].Manipulations);
_editor.SetFiles(result.Default, files);
_editor.SetFileSwaps(result.Default, mods[0].FileSwaps);
_editor.SetManipulations(result.Default, mods[0].Manipulations);
}
else
{
var (group, groupIdx, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name);
var (option, optionIdx, _) = _editor.FindOrAddOption(result, groupIdx, originalOption.GetName());
var folder = Path.Combine(dir.FullName, group.Name, option.Name);
// TODO DataContainer <> Option.
var (group, _, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name);
var (option, _, _) = _editor.FindOrAddOption(group!, originalOption.GetName());
var folder = Path.Combine(dir.FullName, group!.Name, option!.Name);
var files = CopySubModFiles(originalOption, new DirectoryInfo(folder));
_editor.OptionSetFiles(result, groupIdx, optionIdx, files);
_editor.OptionSetFileSwaps(result, groupIdx, optionIdx, originalOption.FileSwaps);
_editor.OptionSetManipulations(result, groupIdx, optionIdx, originalOption.Manipulations);
_editor.SetFiles((IModDataContainer)option, files);
_editor.SetFileSwaps((IModDataContainer)option, originalOption.FileSwaps);
_editor.SetManipulations((IModDataContainer)option, originalOption.Manipulations);
}
}
}
@ -339,16 +341,15 @@ public class ModMerger : IDisposable
{
foreach (var option in _createdOptions)
{
var (groupIdx, optionIdx) = option.GetOptionIndices();
_editor.DeleteOption(MergeToMod!, groupIdx, optionIdx);
_editor.DeleteOption(option);
Penumbra.Log.Verbose($"[Merger] Removed option {option.FullName}.");
}
foreach (var group in _createdGroups)
{
var groupName = MergeToMod!.Groups[group];
_editor.DeleteModGroup(MergeToMod!, group);
Penumbra.Log.Verbose($"[Merger] Removed option group {groupName}.");
_editor.DeleteModGroup(groupName);
Penumbra.Log.Verbose($"[Merger] Removed option group {groupName.Name}.");
}
foreach (var dir in _createdDirectories)

View file

@ -145,12 +145,12 @@ public class ModMetaEditor(ModManager modManager)
Split(currentOption.Manipulations);
}
public void Apply(Mod mod, int groupIdx, int optionIdx)
public void Apply(IModDataContainer container)
{
if (!Changes)
return;
modManager.OptionEditor.OptionSetManipulations(mod, groupIdx, optionIdx, Recombine().ToHashSet());
modManager.OptionEditor.SetManipulations(container, Recombine().ToHashSet());
Changes = false;
}

View file

@ -283,12 +283,12 @@ public class ModNormalizer(ModManager _modManager, Configuration _config)
switch (group)
{
case SingleModGroup single:
foreach (var (_, optionIdx) in single.OptionData.WithIndex())
_modManager.OptionEditor.OptionSetFiles(Mod, groupIdx, optionIdx, _redirections[groupIdx + 1][optionIdx]);
foreach (var (option, optionIdx) in single.OptionData.WithIndex())
_modManager.OptionEditor.SetFiles(option, _redirections[groupIdx + 1][optionIdx]);
break;
case MultiModGroup multi:
foreach (var (_, optionIdx) in multi.OptionData.WithIndex())
_modManager.OptionEditor.OptionSetFiles(Mod, groupIdx, optionIdx, _redirections[groupIdx + 1][optionIdx]);
foreach (var (option, optionIdx) in multi.OptionData.WithIndex())
_modManager.OptionEditor.SetFiles(option, _redirections[groupIdx + 1][optionIdx]);
break;
}
}

View file

@ -17,12 +17,12 @@ public class ModSwapEditor(ModManager modManager)
Changes = false;
}
public void Apply(Mod mod, int groupIdx, int optionIdx)
public void Apply(IModDataContainer container)
{
if (!Changes)
return;
modManager.OptionEditor.OptionSetFileSwaps(mod, groupIdx, optionIdx, _swaps);
modManager.OptionEditor.SetFileSwaps(container, _swaps);
Changes = false;
}

View file

@ -9,7 +9,7 @@ namespace Penumbra.Mods.Groups;
public interface ITexToolsGroup
{
public IReadOnlyList<IModDataOption> OptionData { get; }
public IReadOnlyList<OptionSubMod> OptionData { get; }
}
public interface IModGroup
@ -17,22 +17,19 @@ public interface IModGroup
public const int MaxMultiOptions = 63;
public Mod Mod { get; }
public string Name { get; }
public string Name { get; set; }
public string Description { get; set; }
public GroupType Type { get; }
public ModPriority Priority { get; set; }
public Setting DefaultSettings { get; set; }
public FullPath? FindBestMatch(Utf8GamePath gamePath);
public int AddOption(Mod mod, string name, string description = "");
public FullPath? FindBestMatch(Utf8GamePath gamePath);
public IModOption? AddOption(string name, string description = "");
public IReadOnlyList<IModOption> Options { get; }
public IReadOnlyList<IModDataContainer> DataContainers { get; }
public bool IsOption { get; }
public IModGroup Convert(GroupType type);
public bool MoveOption(int optionIdxFrom, int optionIdxTo);
public int GetIndex();
public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations);

View file

@ -0,0 +1,158 @@
using Newtonsoft.Json;
using Penumbra.Api.Enums;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Settings;
using Penumbra.Mods.SubMods;
using Penumbra.String.Classes;
namespace Penumbra.Mods.Groups;
public class ImcModGroup(Mod mod) : IModGroup
{
public const int DisabledIndex = 30;
public const int NumAttributes = 10;
public Mod Mod { get; } = mod;
public string Name { get; set; } = "Option";
public string Description { get; set; } = "A single IMC manipulation.";
public GroupType Type
=> GroupType.Imc;
public ModPriority Priority { get; set; } = ModPriority.Default;
public Setting DefaultSettings { get; set; } = Setting.Zero;
public PrimaryId PrimaryId;
public SecondaryId SecondaryId;
public ObjectType ObjectType;
public BodySlot BodySlot;
public EquipSlot EquipSlot;
public Variant Variant;
public ImcEntry DefaultEntry;
public FullPath? FindBestMatch(Utf8GamePath gamePath)
=> null;
private bool _canBeDisabled = false;
public bool CanBeDisabled
{
get => _canBeDisabled;
set
{
_canBeDisabled = value;
if (!value)
DefaultSettings = FixSetting(DefaultSettings);
}
}
public IModOption? AddOption(string name, string description = "")
{
uint fullMask = GetFullMask();
var firstUnset = (byte)BitOperations.TrailingZeroCount(~fullMask);
// All attributes handled.
if (firstUnset >= NumAttributes)
return null;
var groupIdx = Mod.Groups.IndexOf(this);
if (groupIdx < 0)
return null;
var subMod = new ImcSubMod(this)
{
Name = name,
Description = description,
AttributeIndex = firstUnset,
};
OptionData.Add(subMod);
return subMod;
}
public readonly List<ImcSubMod> OptionData = [];
public IReadOnlyList<IModOption> Options
=> OptionData;
public IReadOnlyList<IModDataContainer> DataContainers
=> [];
public bool IsOption
=> CanBeDisabled || OptionData.Count > 0;
public int GetIndex()
=> ModGroup.GetIndex(this);
private ushort GetCurrentMask(Setting setting)
{
var mask = DefaultEntry.AttributeMask;
for (var i = 0; i < OptionData.Count; ++i)
{
if (!setting.HasFlag(i))
continue;
var option = OptionData[i];
mask |= option.Attribute;
}
return mask;
}
private ushort GetFullMask()
=> GetCurrentMask(Setting.AllBits(63));
private ImcManipulation GetManip(ushort mask)
=> new(ObjectType, BodySlot, PrimaryId, SecondaryId.Id, Variant.Id, EquipSlot,
DefaultEntry with { AttributeMask = mask });
public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations)
{
if (CanBeDisabled && setting.HasFlag(DisabledIndex))
return;
var mask = GetCurrentMask(setting);
var imc = GetManip(mask);
manipulations.Add(imc);
}
public Setting FixSetting(Setting setting)
=> new(setting.Value & (((1ul << OptionData.Count) - 1) | (CanBeDisabled ? 1ul << DisabledIndex : 0)));
public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null)
{
ModSaveGroup.WriteJsonBase(jWriter, this);
jWriter.WritePropertyName(nameof(ObjectType));
jWriter.WriteValue(ObjectType.ToString());
jWriter.WritePropertyName(nameof(BodySlot));
jWriter.WriteValue(BodySlot.ToString());
jWriter.WritePropertyName(nameof(EquipSlot));
jWriter.WriteValue(EquipSlot.ToString());
jWriter.WritePropertyName(nameof(PrimaryId));
jWriter.WriteValue(PrimaryId.Id);
jWriter.WritePropertyName(nameof(SecondaryId));
jWriter.WriteValue(SecondaryId.Id);
jWriter.WritePropertyName(nameof(Variant));
jWriter.WriteValue(Variant.Id);
jWriter.WritePropertyName(nameof(DefaultEntry));
serializer.Serialize(jWriter, DefaultEntry);
jWriter.WritePropertyName("Options");
jWriter.WriteStartArray();
foreach (var option in OptionData)
{
jWriter.WriteStartObject();
SubMod.WriteModOption(jWriter, option);
jWriter.WritePropertyName(nameof(option.AttributeIndex));
jWriter.WriteValue(option.AttributeIndex);
jWriter.WriteEndObject();
}
jWriter.WriteEndArray();
jWriter.WriteEndObject();
}
public (int Redirections, int Swaps, int Manips) GetCounts()
=> (0, 0, 1);
}

View file

@ -5,6 +5,7 @@ namespace Penumbra.Mods.Groups;
public static class ModGroup
{
/// <summary> Create a new mod group based on the given type. </summary>
public static IModGroup Create(Mod mod, GroupType type, string name)
{
var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1;
@ -20,6 +21,11 @@ public static class ModGroup
Name = name,
Priority = maxPriority,
},
GroupType.Imc => new ImcModGroup(mod)
{
Name = name,
Priority = maxPriority,
},
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null),
};
}
@ -38,5 +44,14 @@ public static class ModGroup
}
return (redirectionCount, swapCount, manipCount);
}
public static int GetIndex(IModGroup group)
{
var groupIndex = group.Mod.Groups.IndexOf(group);
if (groupIndex < 0)
throw new Exception($"Mod {group.Mod.Name} from Group {group.Name} does not contain this group.");
return groupIndex;
}
}

View file

@ -1,9 +1,9 @@
using Newtonsoft.Json;
using Penumbra.Mods.SubMods;
using Penumbra.Services;
namespace Penumbra.Mods.Groups;
using Newtonsoft.Json;
using Penumbra.Mods.SubMods;
using Penumbra.Services;
namespace Penumbra.Mods.Groups;
public readonly struct ModSaveGroup : ISavable
{
private readonly DirectoryInfo _basePath;
@ -12,25 +12,21 @@ public readonly struct ModSaveGroup : ISavable
private readonly DefaultSubMod? _defaultMod;
private readonly bool _onlyAscii;
public ModSaveGroup(Mod mod, int groupIdx, bool onlyAscii)
{
_basePath = mod.ModPath;
_groupIdx = groupIdx;
if (_groupIdx < 0)
_defaultMod = mod.Default;
else
_group = mod.Groups[_groupIdx];
_onlyAscii = onlyAscii;
}
public ModSaveGroup(DirectoryInfo basePath, IModGroup group, int groupIdx, bool onlyAscii)
private ModSaveGroup(DirectoryInfo basePath, IModGroup group, int groupIndex, bool onlyAscii)
{
_basePath = basePath;
_group = group;
_groupIdx = groupIdx;
_groupIdx = groupIndex;
_onlyAscii = onlyAscii;
}
public static ModSaveGroup WithoutMod(DirectoryInfo basePath, IModGroup group, int groupIndex, bool onlyAscii)
=> new(basePath, group, groupIndex, onlyAscii);
public ModSaveGroup(IModGroup group, bool onlyAscii)
: this(group.Mod.ModPath, group, group.GetIndex(), onlyAscii)
{ }
public ModSaveGroup(DirectoryInfo basePath, DefaultSubMod @default, bool onlyAscii)
{
_basePath = basePath;
@ -39,6 +35,33 @@ public readonly struct ModSaveGroup : ISavable
_onlyAscii = onlyAscii;
}
public ModSaveGroup(DirectoryInfo basePath, IModDataContainer container, bool onlyAscii)
{
_basePath = basePath;
_defaultMod = container as DefaultSubMod;
_onlyAscii = onlyAscii;
if (_defaultMod == null)
{
_groupIdx = -1;
_group = null;
}
else
{
_group = container.Group!;
_groupIdx = _group.GetIndex();
}
}
public ModSaveGroup(IModDataContainer container, bool onlyAscii)
{
_basePath = (container.Mod as Mod)?.ModPath
?? throw new Exception("Invalid save group from default data container without base path."); // Should not happen.
_defaultMod = null;
_onlyAscii = onlyAscii;
_group = container.Group!;
_groupIdx = _group.GetIndex();
}
public string ToFilename(FilenameService fileNames)
=> fileNames.OptionGroupFile(_basePath.FullName, _groupIdx, _group?.Name ?? string.Empty, _onlyAscii);
@ -59,7 +82,7 @@ public readonly struct ModSaveGroup : ISavable
{
jWriter.WriteStartObject();
jWriter.WritePropertyName(nameof(group.Name));
jWriter.WriteValue(group!.Name);
jWriter.WriteValue(group.Name);
jWriter.WritePropertyName(nameof(group.Description));
jWriter.WriteValue(group.Description);
jWriter.WritePropertyName(nameof(group.Priority));
@ -69,4 +92,4 @@ public readonly struct ModSaveGroup : ISavable
jWriter.WritePropertyName(nameof(group.DefaultSettings));
jWriter.WriteValue(group.DefaultSettings.Value);
}
}
}

View file

@ -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.Meta.Manipulations;
using Penumbra.Mods.Settings;
@ -18,11 +17,11 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup
public GroupType Type
=> GroupType.Multi;
public Mod Mod { get; set; } = mod;
public string Name { get; set; } = "Group";
public string Description { get; set; } = "A non-exclusive group of settings.";
public ModPriority Priority { get; set; }
public Setting DefaultSettings { get; set; }
public Mod Mod { get; } = mod;
public string Name { get; set; } = "Group";
public string Description { get; set; } = "A non-exclusive group of settings.";
public ModPriority Priority { get; set; }
public Setting DefaultSettings { get; set; }
public readonly List<MultiSubMod> OptionData = [];
public IReadOnlyList<IModOption> Options
@ -39,28 +38,28 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup
.SelectWhere(o => (o.Files.TryGetValue(gamePath, out var file) || o.FileSwaps.TryGetValue(gamePath, out file), file))
.FirstOrDefault();
public int AddOption(Mod mod, string name, string description = "")
public IModOption? AddOption(string name, string description = "")
{
var groupIdx = mod.Groups.IndexOf(this);
var groupIdx = Mod.Groups.IndexOf(this);
if (groupIdx < 0)
return -1;
return null;
var subMod = new MultiSubMod(mod, this)
var subMod = new MultiSubMod(this)
{
Name = name,
Name = name,
Description = description,
};
OptionData.Add(subMod);
return OptionData.Count - 1;
return subMod;
}
public static MultiModGroup? Load(Mod mod, JObject json)
{
var ret = new MultiModGroup(mod)
{
Name = json[nameof(Name)]?.ToObject<string>() ?? string.Empty,
Description = json[nameof(Description)]?.ToObject<string>() ?? string.Empty,
Priority = json[nameof(Priority)]?.ToObject<ModPriority>() ?? ModPriority.Default,
Name = json[nameof(Name)]?.ToObject<string>() ?? string.Empty,
Description = json[nameof(Description)]?.ToObject<string>() ?? string.Empty,
Priority = json[nameof(Priority)]?.ToObject<ModPriority>() ?? ModPriority.Default,
DefaultSettings = json[nameof(DefaultSettings)]?.ToObject<Setting>() ?? Setting.Zero,
};
if (ret.Name.Length == 0)
@ -78,7 +77,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup
break;
}
var subMod = new MultiSubMod(mod, ret, child);
var subMod = new MultiSubMod(ret, child);
ret.OptionData.Add(subMod);
}
@ -87,42 +86,21 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup
return ret;
}
public IModGroup Convert(GroupType type)
public SingleModGroup ConvertToSingle()
{
switch (type)
var single = new SingleModGroup(Mod)
{
case GroupType.Multi: return this;
case GroupType.Single:
var single = new SingleModGroup(Mod)
{
Name = Name,
Description = Description,
Priority = Priority,
DefaultSettings = DefaultSettings.TurnMulti(OptionData.Count),
};
single.OptionData.AddRange(OptionData.Select(o => o.ConvertToSingle(Mod, single)));
return single;
default: throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
}
public bool MoveOption(int optionIdxFrom, int optionIdxTo)
{
if (!OptionData.Move(optionIdxFrom, optionIdxTo))
return false;
DefaultSettings = DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo);
return true;
Name = Name,
Description = Description,
Priority = Priority,
DefaultSettings = DefaultSettings.TurnMulti(OptionData.Count),
};
single.OptionData.AddRange(OptionData.Select(o => o.ConvertToSingle(single)));
return single;
}
public int GetIndex()
{
var groupIndex = Mod.Groups.IndexOf(this);
if (groupIndex < 0)
throw new Exception($"Mod {Mod.Name} from Group {Name} does not contain this group.");
return groupIndex;
}
=> ModGroup.GetIndex(this);
public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations)
{
@ -156,15 +134,15 @@ public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup
=> ModGroup.GetCountsBase(this);
public Setting FixSetting(Setting setting)
=> new(setting.Value & (1ul << OptionData.Count) - 1);
=> new(setting.Value & ((1ul << OptionData.Count) - 1));
/// <summary> Create a group without a mod only for saving it in the creator. </summary>
internal static MultiModGroup CreateForSaving(string name)
internal static MultiModGroup WithoutMod(string name)
=> new(null!)
{
Name = name,
};
IReadOnlyList<IModDataOption> ITexToolsGroup.OptionData
IReadOnlyList<OptionSubMod> ITexToolsGroup.OptionData
=> OptionData;
}

View file

@ -1,7 +1,6 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Filesystem;
using Penumbra.Api.Enums;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Settings;
@ -16,31 +15,28 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup
public GroupType Type
=> GroupType.Single;
public Mod Mod { get; set; } = mod;
public string Name { get; set; } = "Option";
public string Description { get; set; } = "A mutually exclusive group of settings.";
public ModPriority Priority { get; set; }
public Setting DefaultSettings { get; set; }
public Mod Mod { get; } = mod;
public string Name { get; set; } = "Option";
public string Description { get; set; } = "A mutually exclusive group of settings.";
public ModPriority Priority { get; set; }
public Setting DefaultSettings { get; set; }
public readonly List<SingleSubMod> OptionData = [];
IReadOnlyList<IModDataOption> ITexToolsGroup.OptionData
=> OptionData;
public FullPath? FindBestMatch(Utf8GamePath gamePath)
=> OptionData
.SelectWhere(m => (m.Files.TryGetValue(gamePath, out var file) || m.FileSwaps.TryGetValue(gamePath, out file), file))
.FirstOrDefault();
public int AddOption(Mod mod, string name, string description = "")
public IModOption AddOption(string name, string description = "")
{
var subMod = new SingleSubMod(mod, this)
var subMod = new SingleSubMod(this)
{
Name = name,
Name = name,
Description = description,
};
OptionData.Add(subMod);
return OptionData.Count - 1;
return subMod;
}
public IReadOnlyList<IModOption> Options
@ -57,9 +53,9 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup
var options = json["Options"];
var ret = new SingleModGroup(mod)
{
Name = json[nameof(Name)]?.ToObject<string>() ?? string.Empty,
Description = json[nameof(Description)]?.ToObject<string>() ?? string.Empty,
Priority = json[nameof(Priority)]?.ToObject<ModPriority>() ?? ModPriority.Default,
Name = json[nameof(Name)]?.ToObject<string>() ?? string.Empty,
Description = json[nameof(Description)]?.ToObject<string>() ?? string.Empty,
Priority = json[nameof(Priority)]?.ToObject<ModPriority>() ?? ModPriority.Default,
DefaultSettings = json[nameof(DefaultSettings)]?.ToObject<Setting>() ?? Setting.Zero,
};
if (ret.Name.Length == 0)
@ -68,7 +64,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup
if (options != null)
foreach (var child in options.Children())
{
var subMod = new SingleSubMod(mod, ret, child);
var subMod = new SingleSubMod(ret, child);
ret.OptionData.Add(subMod);
}
@ -76,57 +72,21 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup
return ret;
}
public IModGroup Convert(GroupType type)
public MultiModGroup ConvertToMulti()
{
switch (type)
var multi = new MultiModGroup(Mod)
{
case GroupType.Single: return this;
case GroupType.Multi:
var multi = new MultiModGroup(Mod)
{
Name = Name,
Description = Description,
Priority = Priority,
DefaultSettings = Setting.Multi((int)DefaultSettings.Value),
};
multi.OptionData.AddRange(OptionData.Select((o, i) => o.ConvertToMulti(Mod, multi, new ModPriority(i))));
return multi;
default: throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
Name = Name,
Description = Description,
Priority = Priority,
DefaultSettings = Setting.Multi((int)DefaultSettings.Value),
};
multi.OptionData.AddRange(OptionData.Select((o, i) => o.ConvertToMulti(multi, new ModPriority(i))));
return multi;
}
public bool MoveOption(int optionIdxFrom, int optionIdxTo)
{
if (!OptionData.Move(optionIdxFrom, optionIdxTo))
return false;
var currentIndex = DefaultSettings.AsIndex;
// Update default settings with the move.
if (currentIndex == optionIdxFrom)
{
DefaultSettings = Setting.Single(optionIdxTo);
}
else if (optionIdxFrom < optionIdxTo)
{
if (currentIndex > optionIdxFrom && currentIndex <= optionIdxTo)
DefaultSettings = Setting.Single(currentIndex - 1);
}
else if (currentIndex < optionIdxFrom && currentIndex >= optionIdxTo)
{
DefaultSettings = Setting.Single(currentIndex + 1);
}
return true;
}
public int GetIndex()
{
var groupIndex = Mod.Groups.IndexOf(this);
if (groupIndex < 0)
throw new Exception($"Mod {Mod.Name} from Group {Name} does not contain this group.");
return groupIndex;
}
public int GetIndex()
=> ModGroup.GetIndex(this);
public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations)
=> OptionData[setting.AsIndex].AddDataTo(redirections, manipulations);
@ -160,4 +120,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup
{
Name = name,
};
IReadOnlyList<OptionSubMod> ITexToolsGroup.OptionData
=> OptionData;
}

View file

@ -1,3 +1,4 @@
using OtterGui.Classes;
using Penumbra.Collections;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
@ -8,6 +9,7 @@ using Penumbra.Meta;
using Penumbra.Mods.Editor;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Settings;
using Penumbra.Mods.SubMods;
namespace Penumbra.Mods.ItemSwap;
@ -40,8 +42,7 @@ public class ItemSwapContainer
NoSwaps,
}
public bool WriteMod(ModManager manager, Mod mod, WriteType writeType = WriteType.NoSwaps, DirectoryInfo? directory = null,
int groupIndex = -1, int optionIndex = 0)
public bool WriteMod(ModManager manager, Mod mod, IModDataContainer container, WriteType writeType = WriteType.NoSwaps, DirectoryInfo? directory = null)
{
var convertedManips = new HashSet<MetaManipulation>(Swaps.Count);
var convertedFiles = new Dictionary<Utf8GamePath, FullPath>(Swaps.Count);
@ -80,9 +81,9 @@ public class ItemSwapContainer
}
}
manager.OptionEditor.OptionSetFiles(mod, groupIndex, optionIndex, convertedFiles);
manager.OptionEditor.OptionSetFileSwaps(mod, groupIndex, optionIndex, convertedSwaps);
manager.OptionEditor.OptionSetManipulations(mod, groupIndex, optionIndex, convertedManips);
manager.OptionEditor.SetFiles(container, convertedFiles, SaveType.None);
manager.OptionEditor.SetFileSwaps(container, convertedSwaps, SaveType.None);
manager.OptionEditor.SetManipulations(container, convertedManips, SaveType.ImmediateSync);
return true;
}
catch (Exception e)

View file

@ -0,0 +1,38 @@
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;
public sealed class ImcModGroupEditor(CommunicatorService communicator, SaveService saveService, Configuration config)
: ModOptionEditor<ImcModGroup, ImcSubMod>(communicator, saveService, config), IService
{
protected override ImcModGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync)
=> new(mod)
{
Name = newName,
Priority = priority,
};
protected override ImcSubMod? CloneOption(ImcModGroup group, IModOption option)
=> null;
protected override void RemoveOption(ImcModGroup group, int optionIndex)
{
group.OptionData.RemoveAt(optionIndex);
group.DefaultSettings = group.FixSetting(group.DefaultSettings);
}
protected override bool MoveOption(ImcModGroup group, int optionIdxFrom, int optionIdxTo)
{
if (!group.OptionData.Move(optionIdxFrom, optionIdxTo))
return false;
group.DefaultSettings = group.DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo);
return true;
}
}

View file

@ -2,6 +2,8 @@ using Penumbra.Communication;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Groups;
using Penumbra.Mods.SubMods;
using Penumbra.Services;
namespace Penumbra.Mods.Manager;
@ -103,7 +105,7 @@ public class ModCacheManager : IDisposable
}
}
private void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int _, int _2)
private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container, int fromIdx)
{
switch (type)
{

View file

@ -0,0 +1,289 @@
using System.Text.RegularExpressions;
using Dalamud.Interface.Internal.Notifications;
using OtterGui.Classes;
using OtterGui.Filesystem;
using OtterGui.Services;
using Penumbra.Api.Enums;
using Penumbra.GameData.Data;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Groups;
using Penumbra.Mods.Settings;
using Penumbra.Mods.SubMods;
using Penumbra.Services;
using Penumbra.String.Classes;
using Penumbra.Util;
using static FFXIVClientStructs.FFXIV.Client.UI.Misc.ConfigModule;
namespace Penumbra.Mods.Manager;
public enum ModOptionChangeType
{
GroupRenamed,
GroupAdded,
GroupDeleted,
GroupMoved,
GroupTypeChanged,
PriorityChanged,
OptionAdded,
OptionDeleted,
OptionMoved,
OptionFilesChanged,
OptionFilesAdded,
OptionSwapsChanged,
OptionMetaChanged,
DisplayChange,
PrepareChange,
DefaultOptionChanged,
}
public class ModGroupEditor(
SingleModGroupEditor singleEditor,
MultiModGroupEditor multiEditor,
ImcModGroupEditor imcEditor,
CommunicatorService Communicator,
SaveService SaveService,
Configuration Config) : IService
{
public SingleModGroupEditor SingleEditor
=> singleEditor;
public MultiModGroupEditor MultiEditor
=> multiEditor;
public ImcModGroupEditor ImcEditor
=> imcEditor;
/// <summary> Change the settings stored as default options in a mod.</summary>
public void ChangeModGroupDefaultOption(IModGroup group, Setting defaultOption)
{
if (group.DefaultSettings == defaultOption)
return;
group.DefaultSettings = defaultOption;
SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport));
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, group.Mod, group, null, null, -1);
}
/// <summary> Rename an option group if possible. </summary>
public void RenameModGroup(IModGroup group, string newName)
{
var oldName = group.Name;
if (oldName == newName || !VerifyFileName(group.Mod, group, newName, true))
return;
SaveService.ImmediateDelete(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport));
group.Name = newName;
SaveService.ImmediateSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport));
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, group.Mod, group, null, null, -1);
}
/// <summary> Delete a given option group. Fires an event to prepare before actually deleting. </summary>
public void DeleteModGroup(IModGroup group)
{
var mod = group.Mod;
var idx = group.GetIndex();
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, group, null, null, -1);
mod.Groups.RemoveAt(idx);
SaveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport);
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, null, null, null, idx);
}
/// <summary> Move the index of a given option group. </summary>
public void MoveModGroup(IModGroup group, int groupIdxTo)
{
var mod = group.Mod;
var idxFrom = group.GetIndex();
if (!mod.Groups.Move(idxFrom, groupIdxTo))
return;
SaveService.SaveAllOptionGroups(mod, false, Config.ReplaceNonAsciiOnImport);
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, group, null, null, idxFrom);
}
/// <summary> Change the internal priority of the given option group. </summary>
public void ChangeGroupPriority(IModGroup group, ModPriority newPriority)
{
if (group.Priority == newPriority)
return;
group.Priority = newPriority;
SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport));
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, group.Mod, group, null, null, -1);
}
/// <summary> Change the description of the given option group. </summary>
public void ChangeGroupDescription(IModGroup group, string newDescription)
{
if (group.Description == newDescription)
return;
group.Description = newDescription;
SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport));
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, group.Mod, group, null, null, -1);
}
/// <summary> Rename the given option. </summary>
public void RenameOption(IModOption option, string newName)
{
if (option.Name == newName)
return;
option.Name = newName;
SaveService.QueueSave(new ModSaveGroup(option.Group, Config.ReplaceNonAsciiOnImport));
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, option.Mod, option.Group, option, null, -1);
}
/// <summary> Change the description of the given option. </summary>
public void ChangeOptionDescription(IModOption option, string newDescription)
{
if (option.Description == newDescription)
return;
option.Description = newDescription;
SaveService.QueueSave(new ModSaveGroup(option.Group, Config.ReplaceNonAsciiOnImport));
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, option.Mod, option.Group, option, null, -1);
}
/// <summary> Set the meta manipulations for a given option. Replaces existing manipulations. </summary>
public void SetManipulations(IModDataContainer subMod, HashSet<MetaManipulation> manipulations, SaveType saveType = SaveType.Queue)
{
if (subMod.Manipulations.Count == manipulations.Count
&& subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m)))
return;
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1);
subMod.Manipulations.SetTo(manipulations);
SaveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport));
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1);
}
/// <summary> Set the file redirections for a given option. Replaces existing redirections. </summary>
public void SetFiles(IModDataContainer subMod, IReadOnlyDictionary<Utf8GamePath, FullPath> replacements, SaveType saveType = SaveType.Queue)
{
if (subMod.Files.SetEquals(replacements))
return;
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1);
subMod.Files.SetTo(replacements);
SaveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport));
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1);
}
/// <summary> Add additional file redirections to a given option, keeping already existing ones. Only fires an event if anything is actually added.</summary>
public void AddFiles(IModDataContainer subMod, IReadOnlyDictionary<Utf8GamePath, FullPath> additions)
{
var oldCount = subMod.Files.Count;
subMod.Files.AddFrom(additions);
if (oldCount != subMod.Files.Count)
{
SaveService.QueueSave(new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport));
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, (Mod)subMod.Mod, subMod.Group, null, subMod, -1);
}
}
/// <summary> Set the file swaps for a given option. Replaces existing swaps. </summary>
public void SetFileSwaps(IModDataContainer subMod, IReadOnlyDictionary<Utf8GamePath, FullPath> swaps, SaveType saveType = SaveType.Queue)
{
if (subMod.FileSwaps.SetEquals(swaps))
return;
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, (Mod)subMod.Mod, subMod.Group, null, subMod, -1);
subMod.FileSwaps.SetTo(swaps);
SaveService.Save(saveType, new ModSaveGroup(subMod, Config.ReplaceNonAsciiOnImport));
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, (Mod)subMod.Mod, subMod.Group, null, subMod, -1);
}
/// <summary> Verify that a new option group name is unique in this mod. </summary>
public static bool VerifyFileName(Mod mod, IModGroup? group, string newName, bool message)
{
var path = newName.RemoveInvalidPathSymbols();
if (path.Length != 0
&& !mod.Groups.Any(o => !ReferenceEquals(o, group)
&& string.Equals(o.Name.RemoveInvalidPathSymbols(), path, StringComparison.OrdinalIgnoreCase)))
return true;
if (message)
Penumbra.Messager.NotificationMessage(
$"Could not name option {newName} because option with same filename {path} already exists.",
NotificationType.Warning, false);
return false;
}
public void DeleteOption(IModOption option)
{
switch (option)
{
case SingleSubMod s:
SingleEditor.DeleteOption(s);
return;
case MultiSubMod m:
MultiEditor.DeleteOption(m);
return;
case ImcSubMod i:
ImcEditor.DeleteOption(i);
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,
};
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,
};
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, 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),
};
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),
};
public void MoveOption(IModOption option, int toIdx)
{
switch (option)
{
case SingleSubMod s:
SingleEditor.MoveOption(s, toIdx);
return;
case MultiSubMod m:
MultiEditor.MoveOption(m, toIdx);
return;
case ImcSubMod i:
ImcEditor.MoveOption(i, toIdx);
return;
}
}
}

View file

@ -1,4 +1,3 @@
using System.Security.AccessControl;
using Penumbra.Communication;
using Penumbra.Mods.Editor;
using Penumbra.Services;
@ -32,14 +31,14 @@ public sealed class ModManager : ModStorage, IDisposable
private readonly Configuration _config;
private readonly CommunicatorService _communicator;
public readonly ModCreator Creator;
public readonly ModDataEditor DataEditor;
public readonly ModOptionEditor OptionEditor;
public readonly ModCreator Creator;
public readonly ModDataEditor DataEditor;
public readonly ModGroupEditor OptionEditor;
public DirectoryInfo BasePath { get; private set; } = null!;
public bool Valid { get; private set; }
public ModManager(Configuration config, CommunicatorService communicator, ModDataEditor dataEditor, ModOptionEditor optionEditor,
public ModManager(Configuration config, CommunicatorService communicator, ModDataEditor dataEditor, ModGroupEditor optionEditor,
ModCreator creator)
{
_config = config;

View file

@ -83,8 +83,8 @@ public static partial class ModMigration
mod.Default.FileSwaps.Add(gamePath, swapPath);
creator.IncorporateMetaChanges(mod.Default, mod.ModPath, true);
foreach (var (_, index) in mod.Groups.WithIndex())
saveService.ImmediateSave(new ModSaveGroup(mod, index, creator.Config.ReplaceNonAsciiOnImport));
foreach (var group in mod.Groups)
saveService.ImmediateSave(new ModSaveGroup(group, creator.Config.ReplaceNonAsciiOnImport));
// Delete meta files.
foreach (var file in seenMetaFiles.Where(f => f.Exists))
@ -112,7 +112,7 @@ public static partial class ModMigration
}
fileVersion = 1;
saveService.ImmediateSave(new ModSaveGroup(mod, -1, creator.Config.ReplaceNonAsciiOnImport));
saveService.ImmediateSave(new ModSaveGroup(mod.ModPath, mod.Default, creator.Config.ReplaceNonAsciiOnImport));
return true;
}
@ -176,7 +176,7 @@ public static partial class ModMigration
private static SingleSubMod SubModFromOption(ModCreator creator, Mod mod, SingleModGroup group, OptionV0 option,
HashSet<FullPath> seenMetaFiles)
{
var subMod = new SingleSubMod(mod, group)
var subMod = new SingleSubMod(group)
{
Name = option.OptionName,
Description = option.OptionDesc,
@ -189,7 +189,7 @@ public static partial class ModMigration
private static MultiSubMod SubModFromOption(ModCreator creator, Mod mod, MultiModGroup group, OptionV0 option,
ModPriority priority, HashSet<FullPath> seenMetaFiles)
{
var subMod = new MultiSubMod(mod, group)
var subMod = new MultiSubMod(group)
{
Name = option.OptionName,
Description = option.OptionDesc,
@ -219,7 +219,7 @@ public static partial class ModMigration
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
public GroupType SelectionType = GroupType.Single;
public List<OptionV0> Options = new();
public List<OptionV0> Options = [];
public OptionGroupV0()
{ }
@ -236,12 +236,12 @@ public static partial class ModMigration
var token = JToken.Load(reader);
if (token.Type == JTokenType.Array)
return token.ToObject<HashSet<T>>() ?? new HashSet<T>();
return token.ToObject<HashSet<T>>() ?? [];
var tmp = token.ToObject<T>();
return tmp != null
? new HashSet<T> { tmp }
: new HashSet<T>();
: [];
}
public override bool CanWrite

View file

@ -1,384 +1,122 @@
using Dalamud.Interface.Internal.Notifications;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Filesystem;
using Penumbra.Api.Enums;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Groups;
using Penumbra.Mods.Settings;
using Penumbra.Mods.SubMods;
using Penumbra.Services;
using Penumbra.String.Classes;
using Penumbra.Util;
namespace Penumbra.Mods.Manager;
public enum ModOptionChangeType
public abstract class ModOptionEditor<TGroup, TOption>(
CommunicatorService communicator,
SaveService saveService,
Configuration config)
where TGroup : class, IModGroup
where TOption : class, IModOption
{
GroupRenamed,
GroupAdded,
GroupDeleted,
GroupMoved,
GroupTypeChanged,
PriorityChanged,
OptionAdded,
OptionDeleted,
OptionMoved,
OptionFilesChanged,
OptionFilesAdded,
OptionSwapsChanged,
OptionMetaChanged,
DisplayChange,
PrepareChange,
DefaultOptionChanged,
}
public class ModOptionEditor(CommunicatorService communicator, SaveService saveService, Configuration config)
{
/// <summary> Change the type of a group given by mod and index to type, if possible. </summary>
public void ChangeModGroupType(Mod mod, int groupIdx, GroupType type)
{
var group = mod.Groups[groupIdx];
if (group.Type == type)
return;
mod.Groups[groupIdx] = group.Convert(type);
saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1);
}
/// <summary> Change the settings stored as default options in a mod.</summary>
public void ChangeModGroupDefaultOption(Mod mod, int groupIdx, Setting defaultOption)
{
var group = mod.Groups[groupIdx];
if (group.DefaultSettings == defaultOption)
return;
group.DefaultSettings = defaultOption;
saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, mod, groupIdx, -1, -1);
}
/// <summary> Rename an option group if possible. </summary>
public void RenameModGroup(Mod mod, int groupIdx, string newName)
{
var group = mod.Groups[groupIdx];
var oldName = group.Name;
if (oldName == newName || !VerifyFileName(mod, group, newName, true))
return;
saveService.ImmediateDelete(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
_ = group switch
{
SingleModGroup s => s.Name = newName,
MultiModGroup m => m.Name = newName,
_ => newName,
};
saveService.ImmediateSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1);
}
protected readonly CommunicatorService Communicator = communicator;
protected readonly SaveService SaveService = saveService;
protected readonly Configuration Config = config;
/// <summary> Add a new, empty option group of the given type and name. </summary>
public void AddModGroup(Mod mod, GroupType type, string newName, SaveType saveType = SaveType.ImmediateSync)
public TGroup? AddModGroup(Mod mod, string newName, SaveType saveType = SaveType.ImmediateSync)
{
if (!VerifyFileName(mod, null, newName, true))
return;
if (!ModGroupEditor.VerifyFileName(mod, null, newName, true))
return null;
var idx = mod.Groups.Count;
var group = ModGroup.Create(mod, type, newName);
var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1;
var group = CreateGroup(mod, newName, maxPriority);
mod.Groups.Add(group);
saveService.Save(saveType, new ModSaveGroup(mod, idx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, idx, -1, -1);
SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport));
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, group, null, null, -1);
return group;
}
/// <summary> Add a new mod, empty option group of the given type and name if it does not exist already. </summary>
public (IModGroup, int, bool) FindOrAddModGroup(Mod mod, GroupType type, string newName, SaveType saveType = SaveType.ImmediateSync)
public (TGroup, int, bool) FindOrAddModGroup(Mod mod, string newName, SaveType saveType = SaveType.ImmediateSync)
{
var idx = mod.Groups.IndexOf(g => g.Name == newName);
if (idx >= 0)
return (mod.Groups[idx], idx, false);
{
var existingGroup = mod.Groups[idx] as TGroup
?? throw new Exception($"Mod group with name {newName} exists, but is of the wrong type.");
return (existingGroup, idx, false);
}
AddModGroup(mod, type, newName, saveType);
if (mod.Groups[^1].Name != newName)
idx = mod.Groups.Count;
if (AddModGroup(mod, newName, saveType) is not { } group)
throw new Exception($"Could not create new mod group with name {newName}.");
return (mod.Groups[^1], mod.Groups.Count - 1, true);
}
/// <summary> Delete a given option group. Fires an event to prepare before actually deleting. </summary>
public void DeleteModGroup(Mod mod, int groupIdx)
{
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1);
mod.Groups.RemoveAt(groupIdx);
saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport);
communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1);
}
/// <summary> Move the index of a given option group. </summary>
public void MoveModGroup(Mod mod, int groupIdxFrom, int groupIdxTo)
{
if (!mod.Groups.Move(groupIdxFrom, groupIdxTo))
return;
saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport);
communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo);
}
/// <summary> Change the description of the given option group. </summary>
public void ChangeGroupDescription(Mod mod, int groupIdx, string newDescription)
{
var group = mod.Groups[groupIdx];
if (group.Description == newDescription)
return;
group.Description = newDescription;
saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, -1, -1);
}
/// <summary> Change the description of the given option. </summary>
public void ChangeOptionDescription(Mod mod, int groupIdx, int optionIdx, string newDescription)
{
var option = mod.Groups[groupIdx].Options[optionIdx];
if (option.Description == newDescription)
return;
option.Description = newDescription;
saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1);
}
/// <summary> Change the internal priority of the given option group. </summary>
public void ChangeGroupPriority(Mod mod, int groupIdx, ModPriority newPriority)
{
var group = mod.Groups[groupIdx];
if (group.Priority == newPriority)
return;
group.Priority = newPriority;
saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1);
}
/// <summary> Change the internal priority of the given option. </summary>
public void ChangeOptionPriority(Mod mod, int groupIdx, int optionIdx, ModPriority newPriority)
{
switch (mod.Groups[groupIdx])
{
case MultiModGroup multi:
if (multi.OptionData[optionIdx].Priority == newPriority)
return;
multi.OptionData[optionIdx].Priority = newPriority;
saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1);
return;
}
}
/// <summary> Rename the given option. </summary>
public void RenameOption(Mod mod, int groupIdx, int optionIdx, string newName)
{
var option = mod.Groups[groupIdx].Options[optionIdx];
if (option.Name == newName)
return;
option.Name = newName;
saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1);
return (group, idx, true);
}
/// <summary> Add a new empty option of the given name for the given group. </summary>
public int AddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue)
public TOption? AddOption(TGroup group, string newName, SaveType saveType = SaveType.Queue)
{
var group = mod.Groups[groupIdx];
var idx = group.AddOption(mod, newName);
if (idx < 0)
return -1;
if (group.AddOption(newName) is not TOption option)
return null;
saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1);
return idx;
SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport));
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, group.Mod, group, option, null, -1);
return option;
}
/// <summary> Add a new empty option of the given name for the given group if it does not exist already. </summary>
public (IModOption, int, bool) FindOrAddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue)
public (TOption, int, bool) FindOrAddOption(TGroup group, string newName, SaveType saveType = SaveType.Queue)
{
var group = mod.Groups[groupIdx];
var idx = group.Options.IndexOf(o => o.Name == newName);
var idx = group.Options.IndexOf(o => o.Name == newName);
if (idx >= 0)
return (group.Options[idx], idx, false);
{
var existingOption = group.Options[idx] as TOption
?? throw new Exception($"Mod option with name {newName} exists, but is of the wrong type."); // Should never happen.
return (existingOption, idx, false);
}
idx = group.AddOption(mod, newName);
if (idx < 0)
if (AddOption(group, newName, saveType) is not { } option)
throw new Exception($"Could not create new option with name {newName} in {group.Name}.");
saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1);
return (group.Options[idx], idx, true);
return (option, idx, true);
}
/// <summary> Add an existing option to a given group. </summary>
public void AddOption(Mod mod, int groupIdx, IModOption option)
public TOption? AddOption(TGroup group, IModOption option)
{
var group = mod.Groups[groupIdx];
int idx;
switch (group)
{
case MultiModGroup { OptionData.Count: >= IModGroup.MaxMultiOptions }:
Penumbra.Log.Error(
$"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, "
+ $"since only up to {IModGroup.MaxMultiOptions} options are supported in one group.");
return;
case SingleModGroup s:
{
idx = s.OptionData.Count;
var newOption = new SingleSubMod(s.Mod, s)
{
Name = option.Name,
Description = option.Description,
};
if (option is IModDataContainer data)
SubMod.Clone(data, newOption);
s.OptionData.Add(newOption);
break;
}
case MultiModGroup m:
{
idx = m.OptionData.Count;
var newOption = new MultiSubMod(m.Mod, m)
{
Name = option.Name,
Description = option.Description,
Priority = option is MultiSubMod s ? s.Priority : ModPriority.Default,
};
if (option is IModDataContainer data)
SubMod.Clone(data, newOption);
m.OptionData.Add(newOption);
break;
}
default: return;
}
if (CloneOption(group, option) is not { } clonedOption)
return null;
saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1);
SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport));
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, group.Mod, group, clonedOption, null, -1);
return clonedOption;
}
/// <summary> Delete the given option from the given group. </summary>
public void DeleteOption(Mod mod, int groupIdx, int optionIdx)
public void DeleteOption(TOption option)
{
var group = mod.Groups[groupIdx];
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
switch (group)
{
case SingleModGroup s:
s.OptionData.RemoveAt(optionIdx);
break;
case MultiModGroup m:
m.OptionData.RemoveAt(optionIdx);
break;
}
saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1);
var mod = option.Mod;
var group = option.Group;
var optionIdx = option.GetIndex();
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, group, option, null, -1);
RemoveOption((TGroup)group, optionIdx);
SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport));
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, group, null, null, optionIdx);
}
/// <summary> Move an option inside the given option group. </summary>
public void MoveOption(Mod mod, int groupIdx, int optionIdxFrom, int optionIdxTo)
public void MoveOption(TOption option, int optionIdxTo)
{
var group = mod.Groups[groupIdx];
if (!group.MoveOption(optionIdxFrom, optionIdxTo))
var idx = option.GetIndex();
var group = (TGroup)option.Group;
if (!MoveOption(group, idx, optionIdxTo))
return;
saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, mod, groupIdx, optionIdxFrom, optionIdxTo);
SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport));
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, group.Mod, group, option, null, idx);
}
/// <summary> Set the meta manipulations for a given option. Replaces existing manipulations. </summary>
public void OptionSetManipulations(Mod mod, int groupIdx, int dataContainerIdx, HashSet<MetaManipulation> manipulations,
SaveType saveType = SaveType.Queue)
{
var subMod = GetSubMod(mod, groupIdx, dataContainerIdx);
if (subMod.Manipulations.Count == manipulations.Count
&& subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m)))
return;
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, dataContainerIdx, -1);
subMod.Manipulations.SetTo(manipulations);
saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, dataContainerIdx, -1);
}
/// <summary> Set the file redirections for a given option. Replaces existing redirections. </summary>
public void OptionSetFiles(Mod mod, int groupIdx, int dataContainerIdx, IReadOnlyDictionary<Utf8GamePath, FullPath> replacements,
SaveType saveType = SaveType.Queue)
{
var subMod = GetSubMod(mod, groupIdx, dataContainerIdx);
if (subMod.Files.SetEquals(replacements))
return;
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, dataContainerIdx, -1);
subMod.Files.SetTo(replacements);
saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, dataContainerIdx, -1);
}
/// <summary> Add additional file redirections to a given option, keeping already existing ones. Only fires an event if anything is actually added.</summary>
public void OptionAddFiles(Mod mod, int groupIdx, int dataContainerIdx, IReadOnlyDictionary<Utf8GamePath, FullPath> additions)
{
var subMod = GetSubMod(mod, groupIdx, dataContainerIdx);
var oldCount = subMod.Files.Count;
subMod.Files.AddFrom(additions);
if (oldCount != subMod.Files.Count)
{
saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, dataContainerIdx, -1);
}
}
/// <summary> Set the file swaps for a given option. Replaces existing swaps. </summary>
public void OptionSetFileSwaps(Mod mod, int groupIdx, int dataContainerIdx, IReadOnlyDictionary<Utf8GamePath, FullPath> swaps,
SaveType saveType = SaveType.Queue)
{
var subMod = GetSubMod(mod, groupIdx, dataContainerIdx);
if (subMod.FileSwaps.SetEquals(swaps))
return;
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, dataContainerIdx, -1);
subMod.FileSwaps.SetTo(swaps);
saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, dataContainerIdx, -1);
}
/// <summary> Verify that a new option group name is unique in this mod. </summary>
public static bool VerifyFileName(Mod mod, IModGroup? group, string newName, bool message)
{
var path = newName.RemoveInvalidPathSymbols();
if (path.Length != 0
&& !mod.Groups.Any(o => !ReferenceEquals(o, group)
&& string.Equals(o.Name.RemoveInvalidPathSymbols(), path, StringComparison.OrdinalIgnoreCase)))
return true;
if (message)
Penumbra.Messager.NotificationMessage(
$"Could not name option {newName} because option with same filename {path} already exists.",
NotificationType.Warning, false);
return false;
}
/// <summary> Get the correct option for the given group and option index. </summary>
private static IModDataContainer GetSubMod(Mod mod, int groupIdx, int dataContainerIdx)
{
if (groupIdx == -1 && dataContainerIdx == 0)
return mod.Default;
return mod.Groups[groupIdx].DataContainers[dataContainerIdx];
}
protected abstract TGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync);
protected abstract TOption? CloneOption(TGroup group, IModOption option);
protected abstract void RemoveOption(TGroup group, int optionIndex);
protected abstract bool MoveOption(TGroup group, int optionIdxFrom, int optionIdxTo);
}
public static class ModOptionChangeTypeExtension

View file

@ -0,0 +1,84 @@
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;
public sealed class MultiModGroupEditor(CommunicatorService communicator, SaveService saveService, Configuration config)
: ModOptionEditor<MultiModGroup, MultiSubMod>(communicator, saveService, config), IService
{
public void ChangeToSingle(MultiModGroup group)
{
var idx = group.GetIndex();
var singleGroup = group.ConvertToSingle();
group.Mod.Groups[idx] = singleGroup;
SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport));
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, group.Mod, group, null, null, -1);
}
/// <summary> Change the internal priority of the given option. </summary>
public void ChangeOptionPriority(MultiSubMod option, ModPriority newPriority)
{
if (option.Priority == newPriority)
return;
option.Priority = newPriority;
SaveService.QueueSave(new ModSaveGroup(option.Group, Config.ReplaceNonAsciiOnImport));
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, option.Mod, option.Group, option, null, -1);
}
protected override MultiModGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync)
=> new(mod)
{
Name = newName,
Priority = priority,
};
protected override MultiSubMod? CloneOption(MultiModGroup group, IModOption option)
{
if (group.OptionData.Count >= IModGroup.MaxMultiOptions)
{
Penumbra.Log.Error(
$"Could not add option {option.Name} to {group.Name} for mod {group.Mod.Name}, "
+ $"since only up to {IModGroup.MaxMultiOptions} options are supported in one group.");
return null;
}
var newOption = new MultiSubMod(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(MultiModGroup group, int optionIndex)
{
group.OptionData.RemoveAt(optionIndex);
group.DefaultSettings = group.DefaultSettings.RemoveBit(optionIndex);
}
protected override bool MoveOption(MultiModGroup group, int optionIdxFrom, int optionIdxTo)
{
if (!group.OptionData.Move(optionIdxFrom, optionIdxTo))
return false;
group.DefaultSettings = group.DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo);
return true;
}
}

View file

@ -0,0 +1,57 @@
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;
public sealed class SingleModGroupEditor(CommunicatorService communicator, SaveService saveService, Configuration config)
: ModOptionEditor<SingleModGroup, SingleSubMod>(communicator, saveService, config), IService
{
public void ChangeToMulti(SingleModGroup group)
{
var idx = group.GetIndex();
var multiGroup = group.ConvertToMulti();
group.Mod.Groups[idx] = multiGroup;
SaveService.QueueSave(new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport));
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, group.Mod, multiGroup, null, null, -1);
}
protected override SingleModGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync)
=> new(mod)
{
Name = newName,
Priority = priority,
};
protected override SingleSubMod CloneOption(SingleModGroup group, IModOption option)
{
var newOption = new SingleSubMod(group)
{
Name = option.Name,
Description = option.Description,
};
if (option is IModDataContainer data)
SubMod.Clone(data, newOption);
group.OptionData.Add(newOption);
return newOption;
}
protected override void RemoveOption(SingleModGroup group, int optionIndex)
{
group.OptionData.RemoveAt(optionIndex);
group.DefaultSettings = group.DefaultSettings.RemoveSingle(optionIndex);
}
protected override bool MoveOption(SingleModGroup group, int optionIdxFrom, int optionIdxTo)
{
if (!group.OptionData.Move(optionIdxFrom, optionIdxTo))
return false;
group.DefaultSettings = group.DefaultSettings.MoveSingle(optionIdxFrom, optionIdxTo);
return true;
}
}

View file

@ -90,7 +90,7 @@ public partial class ModCreator(
var changes = false;
foreach (var file in _saveService.FileNames.GetOptionGroupFiles(mod))
{
var group = LoadModGroup(mod, file, mod.Groups.Count);
var group = LoadModGroup(mod, file);
if (group != null && mod.Groups.All(g => g.Name != group.Name))
{
changes = changes
@ -244,12 +244,12 @@ public partial class ModCreator(
{
case GroupType.Multi:
{
var group = MultiModGroup.CreateForSaving(name);
var group = MultiModGroup.WithoutMod(name);
group.Description = desc;
group.Priority = priority;
group.DefaultSettings = defaultSettings;
group.OptionData.AddRange(subMods.Select(s => s.Clone(null!, group)));
_saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport));
group.OptionData.AddRange(subMods.Select(s => s.Clone(group)));
_saveService.ImmediateSaveSync(ModSaveGroup.WithoutMod(baseFolder, group, index, Config.ReplaceNonAsciiOnImport));
break;
}
case GroupType.Single:
@ -258,8 +258,8 @@ public partial class ModCreator(
group.Description = desc;
group.Priority = priority;
group.DefaultSettings = defaultSettings;
group.OptionData.AddRange(subMods.Select(s => s.ConvertToSingle(null!, group)));
_saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport));
group.OptionData.AddRange(subMods.Select(s => s.ConvertToSingle(group)));
_saveService.ImmediateSaveSync(ModSaveGroup.WithoutMod(baseFolder, group, index, Config.ReplaceNonAsciiOnImport));
break;
}
}
@ -272,7 +272,7 @@ public partial class ModCreator(
.Select(f => (Utf8GamePath.FromFile(f, optionFolder, out var gamePath, true), gamePath, new FullPath(f)))
.Where(t => t.Item1);
var mod = MultiSubMod.CreateForSaving(option.Name, option.Description, priority);
var mod = MultiSubMod.WithoutGroup(option.Name, option.Description, priority);
foreach (var (_, gamePath, file) in list)
mod.Files.TryAdd(gamePath, file);
@ -295,7 +295,7 @@ public partial class ModCreator(
}
IncorporateMetaChanges(mod.Default, directory, true);
_saveService.ImmediateSaveSync(new ModSaveGroup(mod, -1, Config.ReplaceNonAsciiOnImport));
_saveService.ImmediateSaveSync(new ModSaveGroup(mod.ModPath, mod.Default, Config.ReplaceNonAsciiOnImport));
}
/// <summary> Return the name of a new valid directory based on the base directory and the given name. </summary>
@ -422,7 +422,7 @@ public partial class ModCreator(
/// <summary> Load an option group for a specific mod by its file and index. </summary>
private static IModGroup? LoadModGroup(Mod mod, FileInfo file, int groupIdx)
private static IModGroup? LoadModGroup(Mod mod, FileInfo file)
{
if (!File.Exists(file.FullName))
return null;
@ -442,7 +442,7 @@ public partial class ModCreator(
}
return null;
}
}
internal static void DeleteDeleteList(IEnumerable<string> deleteList, bool delete)
{

View file

@ -4,6 +4,7 @@ using Penumbra.Api.Enums;
using Penumbra.Mods.Editor;
using Penumbra.Mods.Groups;
using Penumbra.Mods.Manager;
using Penumbra.Mods.SubMods;
namespace Penumbra.Mods.Settings;
@ -45,63 +46,64 @@ public class ModSettings
}
// Automatically react to changes in a mods available options.
public bool HandleChanges(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx)
public bool HandleChanges(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, int fromIdx)
{
switch (type)
{
case ModOptionChangeType.GroupRenamed: return true;
case ModOptionChangeType.GroupAdded:
// Add new empty setting for new mod.
Settings.Insert(groupIdx, mod.Groups[groupIdx].DefaultSettings);
Settings.Insert(group!.GetIndex(), group.DefaultSettings);
return true;
case ModOptionChangeType.GroupDeleted:
// Remove setting for deleted mod.
Settings.RemoveAt(groupIdx);
Settings.RemoveAt(fromIdx);
return true;
case ModOptionChangeType.GroupTypeChanged:
{
// Fix settings for a changed group type.
// Single -> Multi: set single as enabled, rest as disabled
// Multi -> Single: set the first enabled option or 0.
var group = mod.Groups[groupIdx];
var config = Settings[groupIdx];
Settings[groupIdx] = group.Type switch
var idx = group!.GetIndex();
var config = Settings[idx];
Settings[idx] = group.Type switch
{
GroupType.Single => config.TurnMulti(group.Options.Count),
GroupType.Multi => Setting.Multi((int)config.Value),
_ => config,
};
return config != Settings[groupIdx];
return config != Settings[idx];
}
case ModOptionChangeType.OptionDeleted:
{
// Single -> select the previous option if any.
// Multi -> excise the corresponding bit.
var group = mod.Groups[groupIdx];
var config = Settings[groupIdx];
Settings[groupIdx] = group.Type switch
var groupIdx = group!.GetIndex();
var config = Settings[groupIdx];
Settings[groupIdx] = group!.Type switch
{
GroupType.Single => config.AsIndex >= optionIdx
? config.AsIndex > 1 ? Setting.Single(config.AsIndex - 1) : Setting.Zero
: config,
GroupType.Multi => config.RemoveBit(optionIdx),
_ => config,
GroupType.Single => config.RemoveSingle(fromIdx),
GroupType.Multi => config.RemoveBit(fromIdx),
GroupType.Imc => config.RemoveBit(fromIdx),
_ => config,
};
return config != Settings[groupIdx];
}
case ModOptionChangeType.GroupMoved:
// Move the group the same way.
return Settings.Move(groupIdx, movedToIdx);
return Settings.Move(fromIdx, group!.GetIndex());
case ModOptionChangeType.OptionMoved:
{
// Single -> select the moved option if it was currently selected
// Multi -> move the corresponding bit
var group = mod.Groups[groupIdx];
var config = Settings[groupIdx];
Settings[groupIdx] = group.Type switch
var groupIdx = group!.GetIndex();
var toIdx = option!.GetIndex();
var config = Settings[groupIdx];
Settings[groupIdx] = group!.Type switch
{
GroupType.Single => config.AsIndex == optionIdx ? Setting.Single(movedToIdx) : config,
GroupType.Multi => config.MoveBit(optionIdx, movedToIdx),
GroupType.Single => config.MoveSingle(fromIdx, toIdx),
GroupType.Multi => config.MoveBit(fromIdx, toIdx),
GroupType.Imc => config.MoveBit(fromIdx, toIdx),
_ => config,
};
return config != Settings[groupIdx];

View file

@ -41,6 +41,34 @@ public readonly record struct Setting(ulong Value)
public Setting TurnMulti(int count)
=> new(Math.Max((ulong)Math.Min(count - 1, BitOperations.TrailingZeroCount(Value)), 0));
public Setting RemoveSingle(int singleIdx)
{
var settingIndex = AsIndex;
if (settingIndex >= singleIdx)
return settingIndex > 1 ? Single(settingIndex - 1) : Zero;
return this;
}
public Setting MoveSingle(int singleIdxFrom, int singleIdxTo)
{
var currentIndex = AsIndex;
if (currentIndex == singleIdxFrom)
return Single(singleIdxTo);
if (singleIdxFrom < singleIdxTo)
{
if (currentIndex > singleIdxFrom && currentIndex <= singleIdxTo)
return Single(currentIndex - 1);
}
else if (currentIndex < singleIdxFrom && currentIndex >= singleIdxTo)
{
return Single(currentIndex + 1);
}
return this;
}
public ModPriority AsPriority
=> new((int)(Value & 0xFFFFFFFF));

View file

@ -1,10 +1,15 @@
using Penumbra.Mods.Groups;
namespace Penumbra.Mods.SubMods;
public interface IModOption
{
public Mod Mod { get; }
public IModGroup Group { get; }
public string Name { get; set; }
public string FullName { get; }
public string Description { get; set; }
public (int GroupIndex, int OptionIndex) GetOptionIndices();
public int GetIndex();
}

View file

@ -0,0 +1,32 @@
using Penumbra.Mods.Groups;
namespace Penumbra.Mods.SubMods;
public class ImcSubMod(ImcModGroup group) : IModOption
{
public readonly ImcModGroup Group = group;
public Mod Mod
=> Group.Mod;
public byte AttributeIndex;
public ushort Attribute
=> (ushort)(1 << AttributeIndex);
Mod IModOption.Mod
=> Mod;
IModGroup IModOption.Group
=> Group;
public string Name { get; set; } = "Part";
public string FullName
=> $"{Group.Name}: {Name}";
public string Description { get; set; } = string.Empty;
public int GetIndex()
=> SubMod.GetIndex(this);
}

View file

@ -4,21 +4,21 @@ using Penumbra.Mods.Settings;
namespace Penumbra.Mods.SubMods;
public class MultiSubMod(Mod mod, MultiModGroup group) : OptionSubMod<MultiModGroup>(mod, group)
public class MultiSubMod(MultiModGroup group) : OptionSubMod<MultiModGroup>(group)
{
public ModPriority Priority { get; set; } = ModPriority.Default;
public MultiSubMod(Mod mod, MultiModGroup group, JToken json)
: this(mod, group)
public MultiSubMod(MultiModGroup group, JToken json)
: this(group)
{
SubMod.LoadOptionData(json, this);
SubMod.LoadDataContainer(json, this, mod.ModPath);
SubMod.LoadDataContainer(json, this, group.Mod.ModPath);
Priority = json[nameof(IModGroup.Priority)]?.ToObject<ModPriority>() ?? ModPriority.Default;
}
public MultiSubMod Clone(Mod mod, MultiModGroup group)
public MultiSubMod Clone(MultiModGroup group)
{
var ret = new MultiSubMod(mod, group)
var ret = new MultiSubMod(group)
{
Name = Name,
Description = Description,
@ -29,9 +29,9 @@ public class MultiSubMod(Mod mod, MultiModGroup group) : OptionSubMod<MultiModGr
return ret;
}
public SingleSubMod ConvertToSingle(Mod mod, SingleModGroup group)
public SingleSubMod ConvertToSingle(SingleModGroup group)
{
var ret = new SingleSubMod(mod, group)
var ret = new SingleSubMod(group)
{
Name = Name,
Description = Description,
@ -40,8 +40,8 @@ public class MultiSubMod(Mod mod, MultiModGroup group) : OptionSubMod<MultiModGr
return ret;
}
public static MultiSubMod CreateForSaving(string name, string description, ModPriority priority)
=> new(null!, null!)
public static MultiSubMod WithoutGroup(string name, string description, ModPriority priority)
=> new(null!)
{
Name = name,
Description = description,

View file

@ -1,25 +1,26 @@
using OtterGui;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor;
using Penumbra.Mods.Groups;
using Penumbra.String.Classes;
using OtterGui;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor;
using Penumbra.Mods.Groups;
using Penumbra.String.Classes;
namespace Penumbra.Mods.SubMods;
public interface IModDataOption : IModDataContainer, IModOption;
public abstract class OptionSubMod<T>(Mod mod, T group) : IModDataOption
where T : IModGroup
public abstract class OptionSubMod(IModGroup group) : IModOption, IModDataContainer
{
internal readonly Mod Mod = mod;
internal readonly IModGroup Group = group;
protected readonly IModGroup Group = group;
public string Name { get; set; } = "Option";
public Mod Mod
=> Group.Mod;
public string Name { get; set; } = "Option";
public string Description { get; set; } = string.Empty;
public string FullName
=> $"{Group!.Name}: {Name}";
=> $"{Group.Name}: {Name}";
public string Description { get; set; } = string.Empty;
Mod IModOption.Mod
=> Mod;
IMod IModDataContainer.Mod
=> Mod;
@ -27,6 +28,9 @@ public abstract class OptionSubMod<T>(Mod mod, T group) : IModDataOption
IModGroup IModDataContainer.Group
=> Group;
IModGroup IModOption.Group
=> Group;
public Dictionary<Utf8GamePath, FullPath> Files { get; set; } = [];
public Dictionary<Utf8GamePath, FullPath> FileSwaps { get; set; } = [];
public HashSet<MetaManipulation> Manipulations { get; set; } = [];
@ -43,8 +47,8 @@ public abstract class OptionSubMod<T>(Mod mod, T group) : IModDataOption
public (int GroupIndex, int DataIndex) GetDataIndices()
=> (Group.GetIndex(), GetDataIndex());
public (int GroupIndex, int OptionIndex) GetOptionIndices()
=> (Group.GetIndex(), GetDataIndex());
public int GetIndex()
=> SubMod.GetIndex(this);
private int GetDataIndex()
{
@ -54,4 +58,11 @@ public abstract class OptionSubMod<T>(Mod mod, T group) : IModDataOption
return dataIndex;
}
}
}
public abstract class OptionSubMod<T>(T group) : OptionSubMod(group)
where T : IModGroup
{
public new T Group
=> (T)base.Group;
}

View file

@ -4,18 +4,18 @@ using Penumbra.Mods.Settings;
namespace Penumbra.Mods.SubMods;
public class SingleSubMod(Mod mod, SingleModGroup singleGroup) : OptionSubMod<SingleModGroup>(mod, singleGroup)
public class SingleSubMod(SingleModGroup singleGroup) : OptionSubMod<SingleModGroup>(singleGroup)
{
public SingleSubMod(Mod mod, SingleModGroup singleGroup, JToken json)
: this(mod, singleGroup)
public SingleSubMod(SingleModGroup singleGroup, JToken json)
: this(singleGroup)
{
SubMod.LoadOptionData(json, this);
SubMod.LoadDataContainer(json, this, mod.ModPath);
SubMod.LoadDataContainer(json, this, singleGroup.Mod.ModPath);
}
public SingleSubMod Clone(Mod mod, SingleModGroup group)
public SingleSubMod Clone(SingleModGroup group)
{
var ret = new SingleSubMod(mod, group)
var ret = new SingleSubMod(group)
{
Name = Name,
Description = Description,
@ -25,9 +25,9 @@ public class SingleSubMod(Mod mod, SingleModGroup singleGroup) : OptionSubMod<Si
return ret;
}
public MultiSubMod ConvertToMulti(Mod mod, MultiModGroup group, ModPriority priority)
public MultiSubMod ConvertToMulti(MultiModGroup group, ModPriority priority)
{
var ret = new MultiSubMod(mod, group)
var ret = new MultiSubMod(group)
{
Name = Name,
Description = Description,

View file

@ -1,30 +1,25 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Groups;
using Penumbra.String.Classes;
namespace Penumbra.Mods.SubMods;
public static class SubMod
{
public static IModOption Create(IModGroup group, string name, string description = "")
=> group switch
{
SingleModGroup single => new SingleSubMod(group.Mod, single)
{
Name = name,
Description = description,
},
MultiModGroup multi => new MultiSubMod(group.Mod, multi)
{
Name = name,
Description = description,
},
_ => throw new ArgumentOutOfRangeException(nameof(group)),
};
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
public static int GetIndex(IModOption option)
{
var dataIndex = option.Group.Options.IndexOf(option);
if (dataIndex < 0)
throw new Exception($"Group {option.Group.Name} from option {option.Name} does not contain this option.");
return dataIndex;
}
/// <summary> Add all unique meta manipulations, file redirections and then file swaps from a ModDataContainer to the given sets. Skip any keys that are already contained. </summary>
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
public static void AddContainerTo(IModDataContainer container, Dictionary<Utf8GamePath, FullPath> redirections,
HashSet<MetaManipulation> manipulations)
{
@ -37,6 +32,7 @@ public static class SubMod
}
/// <summary> Replace all data of <paramref name="to"/> with the data of <paramref name="from"/>. </summary>
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
public static void Clone(IModDataContainer from, IModDataContainer to)
{
to.Files = new Dictionary<Utf8GamePath, FullPath>(from.Files);
@ -45,6 +41,7 @@ public static class SubMod
}
/// <summary> Load all file redirections, file swaps and meta manipulations from a JToken of that option into a data container. </summary>
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
public static void LoadDataContainer(JToken json, IModDataContainer data, DirectoryInfo basePath)
{
data.Files.Clear();
@ -75,6 +72,7 @@ public static class SubMod
}
/// <summary> Load the relevant data for a selectable option from a JToken of that option. </summary>
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
public static void LoadOptionData(JToken json, IModOption option)
{
option.Name = json[nameof(option.Name)]?.ToObject<string>() ?? string.Empty;
@ -82,6 +80,7 @@ public static class SubMod
}
/// <summary> Write file redirections, file swaps and meta manipulations from a data container on a JsonWriter. </summary>
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
public static void WriteModContainer(JsonWriter j, JsonSerializer serializer, IModDataContainer data, DirectoryInfo basePath)
{
j.WritePropertyName(nameof(data.Files));
@ -111,6 +110,7 @@ public static class SubMod
}
/// <summary> Write the data for a selectable mod option on a JsonWriter. </summary>
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
public static void WriteModOption(JsonWriter j, IModOption option)
{
j.WritePropertyName(nameof(option.Name));

View file

@ -23,6 +23,12 @@
<DefineConstants>PROFILING;</DefineConstants>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Mods\Subclasses\**" />
<EmbeddedResource Remove="Mods\Subclasses\**" />
<None Remove="Mods\Subclasses\**" />
</ItemGroup>
<ItemGroup>
<Content Include="tsmLogo.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@ -93,10 +99,6 @@
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="Mods\Subclasses\" />
</ItemGroup>
<Target Name="GetGitHash" BeforeTargets="GetAssemblyVersion" Returns="InformationalVersion">
<Exec Command="git rev-parse --short HEAD" ConsoleToMSBuild="true" StandardOutputImportance="low" ContinueOnError="true">
<Output TaskParameter="ExitCode" PropertyName="GitCommitHashSuccess" />

View file

@ -34,8 +34,11 @@ public sealed class SaveService(Logger log, FrameworkManager framework, Filename
}
}
for (var i = 0; i < mod.Groups.Count - 1; ++i)
ImmediateSave(new ModSaveGroup(mod, i, onlyAscii));
ImmediateSaveSync(new ModSaveGroup(mod, mod.Groups.Count - 1, onlyAscii));
if (mod.Groups.Count > 0)
{
foreach (var group in mod.Groups.SkipLast(1))
ImmediateSave(new ModSaveGroup(group, onlyAscii));
ImmediateSaveSync(new ModSaveGroup(mod.Groups[^1], onlyAscii));
}
}
}

View file

@ -121,7 +121,6 @@ public static class StaticServiceManager
private static ServiceManager AddMods(this ServiceManager services)
=> services.AddSingleton<TempModManager>()
.AddSingleton<ModDataEditor>()
.AddSingleton<ModOptionEditor>()
.AddSingleton<ModCreator>()
.AddSingleton<ModManager>()
.AddSingleton<ModExportManager>()

View file

@ -17,6 +17,7 @@ using Penumbra.Mods.Groups;
using Penumbra.Mods.ItemSwap;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Settings;
using Penumbra.Mods.SubMods;
using Penumbra.Services;
using Penumbra.UI.Classes;
@ -264,9 +265,10 @@ public class ItemSwapTab : IDisposable, ITab
return;
_modManager.AddMod(newDir);
if (!_swapData.WriteMod(_modManager, _modManager[^1],
var mod = _modManager[^1];
if (!_swapData.WriteMod(_modManager, mod, mod.Default,
_useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps))
_modManager.DeleteMod(_modManager[^1]);
_modManager.DeleteMod(mod);
}
private void CreateOption()
@ -276,7 +278,7 @@ public class ItemSwapTab : IDisposable, ITab
var groupCreated = false;
var dirCreated = false;
var optionCreated = -1;
IModOption? createdOption = null;
DirectoryInfo? optionFolderName = null;
try
{
@ -290,22 +292,22 @@ public class ItemSwapTab : IDisposable, ITab
{
if (_selectedGroup == null)
{
_modManager.OptionEditor.AddModGroup(_mod, GroupType.Multi, _newGroupName);
_selectedGroup = _mod.Groups.Last();
if (_modManager.OptionEditor.AddModGroup(_mod, GroupType.Multi, _newGroupName) is not { } group)
throw new Exception($"Failure creating option group.");
_selectedGroup = group;
groupCreated = true;
}
var optionIdx = _modManager.OptionEditor.AddOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _newOptionName);
if (optionIdx < 0)
if (_modManager.OptionEditor.AddOption(_selectedGroup, _newOptionName) is not { } option)
throw new Exception($"Failure creating mod option.");
optionCreated = optionIdx;
createdOption = option;
optionFolderName = Directory.CreateDirectory(optionFolderName.FullName);
dirCreated = true;
if (!_swapData.WriteMod(_modManager, _mod,
_useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps,
optionFolderName,
_mod.Groups.IndexOf(_selectedGroup), optionIdx))
// #TODO ModOption <> DataContainer
if (!_swapData.WriteMod(_modManager, _mod, (IModDataContainer)option,
_useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps, optionFolderName))
throw new Exception("Failure writing files for mod swap.");
}
}
@ -314,12 +316,12 @@ public class ItemSwapTab : IDisposable, ITab
Penumbra.Messager.NotificationMessage(e, "Could not create new Swap Option.", NotificationType.Error, false);
try
{
if (optionCreated >= 0 && _selectedGroup != null)
_modManager.OptionEditor.DeleteOption(_mod, _mod.Groups.IndexOf(_selectedGroup), optionCreated);
if (createdOption != null)
_modManager.OptionEditor.DeleteOption(createdOption);
if (groupCreated)
{
_modManager.OptionEditor.DeleteModGroup(_mod, _mod.Groups.IndexOf(_selectedGroup!));
_modManager.OptionEditor.DeleteModGroup(_selectedGroup!);
_selectedGroup = null;
}
@ -717,7 +719,8 @@ public class ItemSwapTab : IDisposable, ITab
_dirty = true;
}
private void OnModOptionChange(ModOptionChangeType type, Mod mod, int a, int b, int c)
private void OnModOptionChange(ModOptionChangeType type, Mod mod, IModGroup? group, IModOption? option, IModDataContainer? container,
int fromIdx)
{
if (type is ModOptionChangeType.PrepareChange or ModOptionChangeType.GroupAdded or ModOptionChangeType.OptionAdded || mod != _mod)
return;

View file

@ -27,7 +27,7 @@ public partial class ModEditWindow
private const string GenderTooltip = "Gender";
private const string ObjectTypeTooltip = "Object Type";
private const string SecondaryIdTooltip = "Secondary ID";
private const string PrimaryIDTooltip = "Primary ID";
private const string PrimaryIdTooltipShort = "Primary ID";
private const string VariantIdTooltip = "Variant ID";
private const string EstTypeTooltip = "EST Type";
private const string RacialTribeTooltip = "Racial Tribe";
@ -45,7 +45,7 @@ public partial class ModEditWindow
var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option.";
ImGui.NewLine();
if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, setsEqual))
_editor.MetaEditor.Apply(_editor.Mod!, _editor.GroupIdx, _editor.DataIdx);
_editor.MetaEditor.Apply(_editor.Option!);
ImGui.SameLine();
tt = setsEqual ? "No changes staged." : "Revert all currently staged changes.";
@ -477,7 +477,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.PrimaryId.ToString());
ImGuiUtil.HoverTooltip(PrimaryIDTooltip);
ImGuiUtil.HoverTooltip(PrimaryIdTooltipShort);
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);

View file

@ -455,7 +455,7 @@ public partial class ModEditWindow : Window, IDisposable
var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option.";
ImGui.NewLine();
if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, setsEqual))
_editor.SwapEditor.Apply(_editor.Mod!, _editor.GroupIdx, _editor.DataIdx);
_editor.SwapEditor.Apply(_editor.Option!);
ImGui.SameLine();
tt = setsEqual ? "No changes staged." : "Revert all currently staged changes.";
@ -627,7 +627,7 @@ public partial class ModEditWindow : Window, IDisposable
public void Dispose()
{
_communicator.ModPathChanged.Unsubscribe(OnModPathChange);
_editor?.Dispose();
_editor.Dispose();
_materialTab.Dispose();
_modelTab.Dispose();
_shaderPackageTab.Dispose();

View file

@ -15,6 +15,7 @@ using Penumbra.Services;
using Penumbra.UI.AdvancedWindow;
using Penumbra.Mods.Groups;
using Penumbra.Mods.Settings;
using Penumbra.Mods.SubMods;
namespace Penumbra.UI.ModsTab;
@ -248,13 +249,13 @@ public class ModPanelEditTab(
ImGui.SameLine();
var nameValid = ModOptionEditor.VerifyFileName(mod, null, _newGroupName, false);
var nameValid = ModGroupEditor.VerifyFileName(mod, null, _newGroupName, false);
tt = nameValid ? "Add new option group to the mod." : "Can not add a group of this name.";
if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize,
tt, !nameValid, true))
return;
modManager.OptionEditor.AddModGroup(mod, GroupType.Single, _newGroupName);
modManager.OptionEditor.SingleEditor.AddModGroup(mod, _newGroupName);
Reset();
}
}
@ -364,9 +365,9 @@ public class ModPanelEditTab(
break;
case >= 0:
if (_newDescriptionOptionIdx < 0)
modManager.OptionEditor.ChangeGroupDescription(_mod, _newDescriptionIdx, _newDescription);
modManager.OptionEditor.ChangeGroupDescription(_mod.Groups[_newDescriptionIdx], _newDescription);
else
modManager.OptionEditor.ChangeOptionDescription(_mod, _newDescriptionIdx, _newDescriptionOptionIdx,
modManager.OptionEditor.ChangeOptionDescription(_mod.Groups[_newDescriptionIdx].Options[_newDescriptionOptionIdx],
_newDescription);
break;
@ -396,18 +397,18 @@ public class ModPanelEditTab(
.Push(ImGuiStyleVar.ItemSpacing, _itemSpacing);
if (Input.Text("##Name", groupIdx, Input.None, group.Name, out var newGroupName, 256, UiHelpers.InputTextWidth.X))
_modManager.OptionEditor.RenameModGroup(_mod, groupIdx, newGroupName);
_modManager.OptionEditor.RenameModGroup(group, newGroupName);
ImGuiUtil.HoverTooltip("Group Name");
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize,
"Delete this option group.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true))
_delayedActions.Enqueue(() => _modManager.OptionEditor.DeleteModGroup(_mod, groupIdx));
_delayedActions.Enqueue(() => _modManager.OptionEditor.DeleteModGroup(group));
ImGui.SameLine();
if (Input.Priority("##Priority", groupIdx, Input.None, group.Priority, out var priority, 50 * UiHelpers.Scale))
_modManager.OptionEditor.ChangeGroupPriority(_mod, groupIdx, priority);
_modManager.OptionEditor.ChangeGroupPriority(group, priority);
ImGuiUtil.HoverTooltip("Group Priority");
@ -417,7 +418,7 @@ public class ModPanelEditTab(
var tt = groupIdx == 0 ? "Can not move this group further upwards." : $"Move this group up to group {groupIdx}.";
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowUp.ToIconString(), UiHelpers.IconButtonSize,
tt, groupIdx == 0, true))
_delayedActions.Enqueue(() => _modManager.OptionEditor.MoveModGroup(_mod, groupIdx, groupIdx - 1));
_delayedActions.Enqueue(() => _modManager.OptionEditor.MoveModGroup(group, groupIdx - 1));
ImGui.SameLine();
tt = groupIdx == _mod.Groups.Count - 1
@ -425,7 +426,7 @@ public class ModPanelEditTab(
: $"Move this group down to group {groupIdx + 2}.";
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowDown.ToIconString(), UiHelpers.IconButtonSize,
tt, groupIdx == _mod.Groups.Count - 1, true))
_delayedActions.Enqueue(() => _modManager.OptionEditor.MoveModGroup(_mod, groupIdx, groupIdx + 1));
_delayedActions.Enqueue(() => _modManager.OptionEditor.MoveModGroup(group, groupIdx + 1));
ImGui.SameLine();
@ -452,17 +453,17 @@ public class ModPanelEditTab(
{
private const string DragDropLabel = "##DragOption";
private static int _newOptionNameIdx = -1;
private static string _newOptionName = string.Empty;
private static int _dragDropGroupIdx = -1;
private static int _dragDropOptionIdx = -1;
private static int _newOptionNameIdx = -1;
private static string _newOptionName = string.Empty;
private static IModGroup? _dragDropGroup;
private static IModOption? _dragDropOption;
public static void Reset()
{
_newOptionNameIdx = -1;
_newOptionName = string.Empty;
_dragDropGroupIdx = -1;
_dragDropOptionIdx = -1;
_newOptionNameIdx = -1;
_newOptionName = string.Empty;
_dragDropGroup = null;
_dragDropOption = null;
}
public static void Draw(ModPanelEditTab panel, int groupIdx)
@ -482,7 +483,7 @@ public class ModPanelEditTab(
switch (panel._mod.Groups[groupIdx])
{
case SingleModGroup single:
case SingleModGroup single:
for (var optionIdx = 0; optionIdx < single.OptionData.Count; ++optionIdx)
EditOption(panel, single, groupIdx, optionIdx);
break;
@ -491,6 +492,7 @@ public class ModPanelEditTab(
EditOption(panel, multi, groupIdx, optionIdx);
break;
}
DrawNewOption(panel, groupIdx, UiHelpers.IconButtonSize);
}
@ -502,8 +504,8 @@ public class ModPanelEditTab(
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGui.Selectable($"Option #{optionIdx + 1}");
Source(group, groupIdx, optionIdx);
Target(panel, group, groupIdx, optionIdx);
Source(option);
Target(panel, group, optionIdx);
ImGui.TableNextColumn();
@ -511,7 +513,7 @@ public class ModPanelEditTab(
if (group.Type == GroupType.Single)
{
if (ImGui.RadioButton("##default", group.DefaultSettings.AsIndex == optionIdx))
panel._modManager.OptionEditor.ChangeModGroupDefaultOption(panel._mod, groupIdx, Setting.Single(optionIdx));
panel._modManager.OptionEditor.ChangeModGroupDefaultOption(group, Setting.Single(optionIdx));
ImGuiUtil.HoverTooltip($"Set {option.Name} as the default choice for this group.");
}
@ -519,15 +521,14 @@ public class ModPanelEditTab(
{
var isDefaultOption = group.DefaultSettings.HasFlag(optionIdx);
if (ImGui.Checkbox("##default", ref isDefaultOption))
panel._modManager.OptionEditor.ChangeModGroupDefaultOption(panel._mod, groupIdx,
group.DefaultSettings.SetBit(optionIdx, isDefaultOption));
panel._modManager.OptionEditor.ChangeModGroupDefaultOption(group, group.DefaultSettings.SetBit(optionIdx, isDefaultOption));
ImGuiUtil.HoverTooltip($"{(isDefaultOption ? "Disable" : "Enable")} {option.Name} per default in this group.");
}
ImGui.TableNextColumn();
if (Input.Text("##Name", groupIdx, optionIdx, option.Name, out var newOptionName, 256, -1))
panel._modManager.OptionEditor.RenameOption(panel._mod, groupIdx, optionIdx, newOptionName);
panel._modManager.OptionEditor.RenameOption(option, newOptionName);
ImGui.TableNextColumn();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Edit.ToIconString(), UiHelpers.IconButtonSize, "Edit option description.",
@ -537,15 +538,15 @@ public class ModPanelEditTab(
ImGui.TableNextColumn();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize,
"Delete this option.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true))
panel._delayedActions.Enqueue(() => panel._modManager.OptionEditor.DeleteOption(panel._mod, groupIdx, optionIdx));
panel._delayedActions.Enqueue(() => panel._modManager.OptionEditor.DeleteOption(option));
ImGui.TableNextColumn();
if (group is not MultiModGroup multi)
if (option is not MultiSubMod multi)
return;
if (Input.Priority("##Priority", groupIdx, optionIdx, multi.OptionData[optionIdx].Priority, out var priority,
if (Input.Priority("##Priority", groupIdx, optionIdx, multi.Priority, out var priority,
50 * UiHelpers.Scale))
panel._modManager.OptionEditor.ChangeOptionPriority(panel._mod, groupIdx, optionIdx, priority);
panel._modManager.OptionEditor.MultiEditor.ChangeOptionPriority(multi, priority);
ImGuiUtil.HoverTooltip("Option priority.");
}
@ -564,7 +565,7 @@ public class ModPanelEditTab(
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGui.Selectable($"Option #{count + 1}");
Target(panel, group, groupIdx, count);
Target(panel, group, count);
ImGui.TableNextColumn();
ImGui.TableNextColumn();
ImGui.SetNextItemWidth(-1);
@ -585,14 +586,14 @@ public class ModPanelEditTab(
tt, !(canAddGroup && validName), true))
return;
panel._modManager.OptionEditor.AddOption(mod, groupIdx, _newOptionName);
panel._modManager.OptionEditor.AddOption(group, _newOptionName);
_newOptionName = string.Empty;
}
// Handle drag and drop to move options inside a group or into another group.
private static void Source(IModGroup group, int groupIdx, int optionIdx)
private static void Source(IModOption option)
{
if (group is not ITexToolsGroup)
if (option.Group is not ITexToolsGroup)
return;
using var source = ImRaii.DragDropSource();
@ -601,14 +602,14 @@ public class ModPanelEditTab(
if (ImGui.SetDragDropPayload(DragDropLabel, IntPtr.Zero, 0))
{
_dragDropGroupIdx = groupIdx;
_dragDropOptionIdx = optionIdx;
_dragDropGroup = option.Group;
_dragDropOption = option;
}
ImGui.TextUnformatted($"Dragging option {group.Options[optionIdx].Name} from group {group.Name}...");
ImGui.TextUnformatted($"Dragging option {option.Name} from group {option.Group.Name}...");
}
private static void Target(ModPanelEditTab panel, IModGroup group, int groupIdx, int optionIdx)
private static void Target(ModPanelEditTab panel, IModGroup group, int optionIdx)
{
if (group is not ITexToolsGroup)
return;
@ -617,39 +618,53 @@ public class ModPanelEditTab(
if (!target.Success || !ImGuiUtil.IsDropping(DragDropLabel))
return;
if (_dragDropGroupIdx >= 0 && _dragDropOptionIdx >= 0)
if (_dragDropGroup != null && _dragDropOption != null)
{
if (_dragDropGroupIdx == groupIdx)
if (_dragDropGroup == group)
{
var sourceOption = _dragDropOptionIdx;
var sourceOption = _dragDropOption;
panel._delayedActions.Enqueue(
() => panel._modManager.OptionEditor.MoveOption(panel._mod, groupIdx, sourceOption, optionIdx));
() => panel._modManager.OptionEditor.MoveOption(sourceOption, optionIdx));
}
else
{
// Move from one group to another by deleting, then adding, then moving the option.
var sourceGroupIdx = _dragDropGroupIdx;
var sourceOption = _dragDropOptionIdx;
var sourceGroup = panel._mod.Groups[sourceGroupIdx];
var currentCount = group.DataContainers.Count;
var option = ((ITexToolsGroup) sourceGroup).OptionData[_dragDropOptionIdx];
var sourceOption = _dragDropOption;
panel._delayedActions.Enqueue(() =>
{
panel._modManager.OptionEditor.DeleteOption(panel._mod, sourceGroupIdx, sourceOption);
panel._modManager.OptionEditor.AddOption(panel._mod, groupIdx, option);
panel._modManager.OptionEditor.MoveOption(panel._mod, groupIdx, currentCount, optionIdx);
panel._modManager.OptionEditor.DeleteOption(sourceOption);
if (panel._modManager.OptionEditor.AddOption(group, sourceOption) is { } newOption)
panel._modManager.OptionEditor.MoveOption(newOption, optionIdx);
});
}
}
_dragDropGroupIdx = -1;
_dragDropOptionIdx = -1;
_dragDropGroup = null;
_dragDropOption = null;
}
}
/// <summary> Draw a combo to select single or multi group and switch between them. </summary>
private void DrawGroupCombo(IModGroup group, int groupIdx)
{
ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X - 2 * UiHelpers.IconButtonSize.X - 2 * ImGui.GetStyle().ItemSpacing.X);
using var combo = ImRaii.Combo("##GroupType", GroupTypeName(group.Type));
if (!combo)
return;
if (ImGui.Selectable(GroupTypeName(GroupType.Single), group.Type == GroupType.Single) && group is MultiModGroup m)
_modManager.OptionEditor.MultiEditor.ChangeToSingle(m);
var canSwitchToMulti = group.Options.Count <= IModGroup.MaxMultiOptions;
using var style = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, !canSwitchToMulti);
if (ImGui.Selectable(GroupTypeName(GroupType.Multi), group.Type == GroupType.Multi) && canSwitchToMulti && group is SingleModGroup s)
_modManager.OptionEditor.SingleEditor.ChangeToMulti(s);
style.Pop();
if (!canSwitchToMulti)
ImGuiUtil.HoverTooltip($"Can not convert group to multi group since it has more than {IModGroup.MaxMultiOptions} options.");
return;
static string GroupTypeName(GroupType type)
=> type switch
{
@ -657,23 +672,6 @@ public class ModPanelEditTab(
GroupType.Multi => "Multi Group",
_ => "Unknown",
};
ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X - 2 * UiHelpers.IconButtonSize.X - 2 * ImGui.GetStyle().ItemSpacing.X);
using var combo = ImRaii.Combo("##GroupType", GroupTypeName(group.Type));
if (!combo)
return;
if (ImGui.Selectable(GroupTypeName(GroupType.Single), group.Type == GroupType.Single))
_modManager.OptionEditor.ChangeModGroupType(_mod, groupIdx, GroupType.Single);
var canSwitchToMulti = group.Options.Count <= IModGroup.MaxMultiOptions;
using var style = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, !canSwitchToMulti);
if (ImGui.Selectable(GroupTypeName(GroupType.Multi), group.Type == GroupType.Multi) && canSwitchToMulti)
_modManager.OptionEditor.ChangeModGroupType(_mod, groupIdx, GroupType.Multi);
style.Pop();
if (!canSwitchToMulti)
ImGuiUtil.HoverTooltip($"Can not convert group to multi group since it has more than {IModGroup.MaxMultiOptions} options.");
}
/// <summary> Handles input text and integers in separate fields without buffers for every single one. </summary>