mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Now that was a lot of work.
This commit is contained in:
parent
297be487b5
commit
1e5ed1c414
44 changed files with 1182 additions and 766 deletions
|
|
@ -1 +1 @@
|
|||
Subproject commit 590629df33f9ad92baddd1d65ec8c986f18d608a
|
||||
Subproject commit 69d106b457eb0f73d4b4caf1234da5631fd6fbf0
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
158
Penumbra/Mods/Groups/ImcModGroup.cs
Normal file
158
Penumbra/Mods/Groups/ImcModGroup.cs
Normal 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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
38
Penumbra/Mods/Manager/ImcModGroupEditor.cs
Normal file
38
Penumbra/Mods/Manager/ImcModGroupEditor.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
289
Penumbra/Mods/Manager/ModGroupEditor.cs
Normal file
289
Penumbra/Mods/Manager/ModGroupEditor.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
84
Penumbra/Mods/Manager/MultiModGroupEditor.cs
Normal file
84
Penumbra/Mods/Manager/MultiModGroupEditor.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
57
Penumbra/Mods/Manager/SingleModGroupEditor.cs
Normal file
57
Penumbra/Mods/Manager/SingleModGroupEditor.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
32
Penumbra/Mods/SubMods/ImcSubMod.cs
Normal file
32
Penumbra/Mods/SubMods/ImcSubMod.cs
Normal 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);
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue