Rework options, pre-submod types.

This commit is contained in:
Ottermandias 2024-04-23 17:41:55 +02:00
parent 792a04337f
commit 07afbfb229
25 changed files with 620 additions and 300 deletions

View file

@ -56,13 +56,13 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable
var dict = new Dictionary<string, (string[], int)>(mod.Groups.Count); var dict = new Dictionary<string, (string[], int)>(mod.Groups.Count);
foreach (var g in mod.Groups) foreach (var g in mod.Groups)
dict.Add(g.Name, (g.Select(o => o.Name).ToArray(), (int)g.Type)); dict.Add(g.Name, (g.Options.Select(o => o.Name).ToArray(), (int)g.Type));
return new AvailableModSettings(dict); return new AvailableModSettings(dict);
} }
public Dictionary<string, (string[], int)>? GetAvailableModSettingsBase(string modDirectory, string modName) public Dictionary<string, (string[], int)>? GetAvailableModSettingsBase(string modDirectory, string modName)
=> _modManager.TryGetMod(modDirectory, modName, out var mod) => _modManager.TryGetMod(modDirectory, modName, out var mod)
? mod.Groups.ToDictionary(g => g.Name, g => (g.Select(o => o.Name).ToArray(), (int)g.Type)) ? mod.Groups.ToDictionary(g => g.Name, g => (g.Options.Select(o => o.Name).ToArray(), (int)g.Type))
: null; : null;
public (PenumbraApiEc, (bool, int, Dictionary<string, List<string>>, bool)?) GetCurrentModSettings(Guid collectionId, string modDirectory, public (PenumbraApiEc, (bool, int, Dictionary<string, List<string>>, bool)?) GetCurrentModSettings(Guid collectionId, string modDirectory,
@ -153,7 +153,7 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable
if (groupIdx < 0) if (groupIdx < 0)
return ApiHelpers.Return(PenumbraApiEc.OptionGroupMissing, args); return ApiHelpers.Return(PenumbraApiEc.OptionGroupMissing, args);
var optionIdx = mod.Groups[groupIdx].IndexOf(o => o.Name == optionName); var optionIdx = mod.Groups[groupIdx].Options.IndexOf(o => o.Name == optionName);
if (optionIdx < 0) if (optionIdx < 0)
return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args);
@ -190,7 +190,7 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable
{ {
case SingleModGroup single: case SingleModGroup single:
{ {
var optionIdx = optionNames.Count == 0 ? -1 : single.IndexOf(o => o.Name == optionNames[^1]); var optionIdx = optionNames.Count == 0 ? -1 : single.OptionData.IndexOf(o => o.Name == optionNames[^1]);
if (optionIdx < 0) if (optionIdx < 0)
return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args);
@ -201,7 +201,7 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable
{ {
foreach (var name in optionNames) foreach (var name in optionNames)
{ {
var optionIdx = multi.IndexOf(o => o.Name == name); var optionIdx = multi.PrioritizedOptions.IndexOf(o => o.Mod.Name == name);
if (optionIdx < 0) if (optionIdx < 0)
return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args); return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args);

View file

@ -203,7 +203,7 @@ public partial class TexToolsImporter
{ {
var option = group.OptionList[idx]; var option = group.OptionList[idx];
_currentOptionName = option.Name; _currentOptionName = option.Name;
options.Insert(idx, ModCreator.CreateEmptySubMod(option.Name)); options.Insert(idx, SubMod.CreateForSaving(option.Name));
if (option.IsChecked) if (option.IsChecked)
defaultSettings = Setting.Single(idx); defaultSettings = Setting.Single(idx);
} }

View file

@ -54,7 +54,13 @@ public unsafe class MetaFileManager
if (!dir.Exists) if (!dir.Exists)
dir.Create(); dir.Create();
foreach (var option in group.OfType<SubMod>()) var optionEnumerator = group switch
{
SingleModGroup single => single.OptionData,
MultiModGroup multi => multi.PrioritizedOptions.Select(o => o.Mod),
_ => [],
};
foreach (var option in optionEnumerator)
{ {
var optionDir = ModCreator.NewOptionDirectory(dir, option.Name, Config.ReplaceNonAsciiOnImport); var optionDir = ModCreator.NewOptionDirectory(dir, option.Name, Config.ReplaceNonAsciiOnImport);
if (!optionDir.Exists) if (!optionDir.Exists)

View file

@ -1,3 +1,4 @@
using System;
using OtterGui; using OtterGui;
using OtterGui.Compression; using OtterGui.Compression;
using Penumbra.Mods.Subclasses; using Penumbra.Mods.Subclasses;
@ -72,9 +73,15 @@ public class ModEditor(
if (groupIdx >= 0) if (groupIdx >= 0)
{ {
Group = Mod.Groups[groupIdx]; Group = Mod.Groups[groupIdx];
if (optionIdx >= 0 && optionIdx < Group.Count) switch(Group)
{ {
Option = Group[optionIdx]; case SingleModGroup single when optionIdx >= 0 && optionIdx < single.OptionData.Count:
Option = single.OptionData[optionIdx];
GroupIdx = groupIdx;
OptionIdx = optionIdx;
return;
case MultiModGroup multi when optionIdx >= 0 && optionIdx < multi.PrioritizedOptions.Count:
Option = multi.PrioritizedOptions[optionIdx].Mod;
GroupIdx = groupIdx; GroupIdx = groupIdx;
OptionIdx = optionIdx; OptionIdx = optionIdx;
return; return;
@ -109,8 +116,17 @@ public class ModEditor(
action(mod.Default, -1, 0); action(mod.Default, -1, 0);
foreach (var (group, groupIdx) in mod.Groups.WithIndex()) foreach (var (group, groupIdx) in mod.Groups.WithIndex())
{ {
for (var optionIdx = 0; optionIdx < group.Count; ++optionIdx) switch (group)
action(group[optionIdx], groupIdx, optionIdx); {
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.PrioritizedOptions.Count; ++optionIdx)
action(multi.PrioritizedOptions[optionIdx].Mod, groupIdx, optionIdx);
break;
}
} }
} }

View file

@ -24,7 +24,8 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu
num += dict.TryAdd(path.Item2, file.File) ? 0 : 1; num += dict.TryAdd(path.Item2, file.File) ? 0 : 1;
} }
modManager.OptionEditor.OptionSetFiles(mod, option.GroupIdx, option.OptionIdx, dict); var (groupIdx, optionIdx) = option.GetIndices();
modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, dict);
files.UpdatePaths(mod, option); files.UpdatePaths(mod, option);
Changes = false; Changes = false;
return num; return num;

View file

@ -1,5 +1,6 @@
using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.Notifications;
using Dalamud.Utility; using Dalamud.Utility;
using ImGuizmoNET;
using OtterGui; using OtterGui;
using OtterGui.Classes; using OtterGui.Classes;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
@ -104,9 +105,16 @@ public class ModMerger : IDisposable
((List<string>)Warnings).Add( ((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}."); $"The merged group {group.Name} already existed, but has a different type {group.Type} than the original group of type {originalGroup.Type}.");
foreach (var originalOption in originalGroup) var optionEnumerator = group switch
{ {
var (option, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, originalOption.Name); SingleModGroup single => single.OptionData,
MultiModGroup multi => multi.PrioritizedOptions.Select(o => o.Mod),
_ => [],
};
foreach (var originalOption in optionEnumerator)
{
var (option, _, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, originalOption.Name);
if (optionCreated) if (optionCreated)
{ {
_createdOptions.Add(option); _createdOptions.Add(option);
@ -138,7 +146,7 @@ public class ModMerger : IDisposable
var (group, groupIdx, groupCreated) = _editor.FindOrAddModGroup(MergeToMod!, GroupType.Multi, groupName, SaveType.None); var (group, groupIdx, groupCreated) = _editor.FindOrAddModGroup(MergeToMod!, GroupType.Multi, groupName, SaveType.None);
if (groupCreated) if (groupCreated)
_createdGroups.Add(groupIdx); _createdGroups.Add(groupIdx);
var (option, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, optionName, SaveType.None); var (option, _, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, optionName, SaveType.None);
if (optionCreated) if (optionCreated)
_createdOptions.Add(option); _createdOptions.Add(option);
var dir = ModCreator.NewOptionDirectory(MergeToMod!.ModPath, groupName, _config.ReplaceNonAsciiOnImport); var dir = ModCreator.NewOptionDirectory(MergeToMod!.ModPath, groupName, _config.ReplaceNonAsciiOnImport);
@ -184,9 +192,10 @@ public class ModMerger : IDisposable
} }
} }
_editor.OptionSetFiles(MergeToMod!, option.GroupIdx, option.OptionIdx, redirections, SaveType.None); var (groupIdx, optionIdx) = option.GetIndices();
_editor.OptionSetFileSwaps(MergeToMod!, option.GroupIdx, option.OptionIdx, swaps, SaveType.None); _editor.OptionSetFiles(MergeToMod!, groupIdx, optionIdx, redirections, SaveType.None);
_editor.OptionSetManipulations(MergeToMod!, option.GroupIdx, option.OptionIdx, manips, SaveType.ImmediateSync); _editor.OptionSetFileSwaps(MergeToMod!, groupIdx, optionIdx, swaps, SaveType.None);
_editor.OptionSetManipulations(MergeToMod!, groupIdx, optionIdx, manips, SaveType.ImmediateSync);
return; return;
bool GetFullPath(FullPath input, out FullPath ret) bool GetFullPath(FullPath input, out FullPath ret)
@ -251,7 +260,7 @@ public class ModMerger : IDisposable
Mod? result = null; Mod? result = null;
try try
{ {
dir = _creator.CreateEmptyMod(_mods.BasePath, modName, $"Split off from {mods[0].ParentMod.Name}."); dir = _creator.CreateEmptyMod(_mods.BasePath, modName, $"Split off from {mods[0].Mod.Name}.");
if (dir == null) if (dir == null)
throw new Exception($"Could not split off mods, unable to create new mod with name {modName}."); throw new Exception($"Could not split off mods, unable to create new mod with name {modName}.");
@ -268,7 +277,6 @@ public class ModMerger : IDisposable
{ {
foreach (var originalOption in mods) foreach (var originalOption in mods)
{ {
var originalGroup = originalOption.ParentMod.Groups[originalOption.GroupIdx];
if (originalOption.IsDefault) if (originalOption.IsDefault)
{ {
var files = CopySubModFiles(mods[0], dir); var files = CopySubModFiles(mods[0], dir);
@ -278,13 +286,14 @@ public class ModMerger : IDisposable
} }
else else
{ {
var originalGroup = originalOption.Group;
var (group, groupIdx, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name); var (group, groupIdx, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name);
var (option, _) = _editor.FindOrAddOption(result, groupIdx, originalOption.Name); var (option, optionIdx, _) = _editor.FindOrAddOption(result, groupIdx, originalOption.Name);
var folder = Path.Combine(dir.FullName, group.Name, option.Name); var folder = Path.Combine(dir.FullName, group.Name, option.Name);
var files = CopySubModFiles(originalOption, new DirectoryInfo(folder)); var files = CopySubModFiles(originalOption, new DirectoryInfo(folder));
_editor.OptionSetFiles(result, groupIdx, option.OptionIdx, files); _editor.OptionSetFiles(result, groupIdx, optionIdx, files);
_editor.OptionSetFileSwaps(result, groupIdx, option.OptionIdx, originalOption.FileSwapData); _editor.OptionSetFileSwaps(result, groupIdx, optionIdx, originalOption.FileSwapData);
_editor.OptionSetManipulations(result, groupIdx, option.OptionIdx, originalOption.ManipulationData); _editor.OptionSetManipulations(result, groupIdx, optionIdx, originalOption.ManipulationData);
} }
} }
} }
@ -309,7 +318,7 @@ public class ModMerger : IDisposable
private static Dictionary<Utf8GamePath, FullPath> CopySubModFiles(SubMod option, DirectoryInfo newMod) private static Dictionary<Utf8GamePath, FullPath> CopySubModFiles(SubMod option, DirectoryInfo newMod)
{ {
var ret = new Dictionary<Utf8GamePath, FullPath>(option.FileData.Count); var ret = new Dictionary<Utf8GamePath, FullPath>(option.FileData.Count);
var parentPath = ((Mod)option.ParentMod).ModPath.FullName; var parentPath = ((Mod)option.Mod).ModPath.FullName;
foreach (var (path, file) in option.FileData) foreach (var (path, file) in option.FileData)
{ {
var target = Path.GetRelativePath(parentPath, file.FullName); var target = Path.GetRelativePath(parentPath, file.FullName);
@ -339,7 +348,8 @@ public class ModMerger : IDisposable
{ {
foreach (var option in _createdOptions) foreach (var option in _createdOptions)
{ {
_editor.DeleteOption(MergeToMod!, option.GroupIdx, option.OptionIdx); var (groupIdx, optionIdx) = option.GetIndices();
_editor.DeleteOption(MergeToMod!, groupIdx, optionIdx);
Penumbra.Log.Verbose($"[Merger] Removed option {option.FullName}."); Penumbra.Log.Verbose($"[Merger] Removed option {option.FullName}.");
} }

View file

@ -167,16 +167,43 @@ public class ModNormalizer(ModManager _modManager, Configuration _config)
// Normalize all other options. // Normalize all other options.
foreach (var (group, groupIdx) in Mod.Groups.WithIndex()) foreach (var (group, groupIdx) in Mod.Groups.WithIndex())
{ {
_redirections[groupIdx + 1].EnsureCapacity(group.Count); var groupDir = ModCreator.CreateModFolder(directory, group.Name, _config.ReplaceNonAsciiOnImport, true);
for (var i = _redirections[groupIdx + 1].Count; i < group.Count; ++i) switch (group)
{
case SingleModGroup single:
_redirections[groupIdx + 1].EnsureCapacity(single.OptionData.Count);
for (var i = _redirections[groupIdx + 1].Count; i < single.OptionData.Count; ++i)
_redirections[groupIdx + 1].Add([]); _redirections[groupIdx + 1].Add([]);
var groupDir = ModCreator.CreateModFolder(directory, group.Name, _config.ReplaceNonAsciiOnImport, true); foreach (var (option, optionIdx) in single.OptionData.WithIndex())
foreach (var option in group.OfType<SubMod>()) HandleSubMod(groupDir, option, _redirections[groupIdx + 1][optionIdx]);
break;
case MultiModGroup multi:
_redirections[groupIdx + 1].EnsureCapacity(multi.PrioritizedOptions.Count);
for (var i = _redirections[groupIdx + 1].Count; i < multi.PrioritizedOptions.Count; ++i)
_redirections[groupIdx + 1].Add([]);
foreach (var ((option, _), optionIdx) in multi.PrioritizedOptions.WithIndex())
HandleSubMod(groupDir, option, _redirections[groupIdx + 1][optionIdx]);
break;
}
}
return true;
}
catch (Exception e)
{
Penumbra.Messager.NotificationMessage(e, $"Could not normalize mod {Mod.Name}.", NotificationType.Error, false);
}
return false;
void HandleSubMod(DirectoryInfo groupDir, SubMod option, Dictionary<Utf8GamePath, FullPath> newDict)
{ {
var optionDir = ModCreator.CreateModFolder(groupDir, option.Name, _config.ReplaceNonAsciiOnImport, true); var optionDir = ModCreator.CreateModFolder(groupDir, option.Name, _config.ReplaceNonAsciiOnImport, true);
newDict = _redirections[groupIdx + 1][option.OptionIdx];
newDict.Clear(); newDict.Clear();
newDict.EnsureCapacity(option.FileData.Count); newDict.EnsureCapacity(option.FileData.Count);
foreach (var (gamePath, fullPath) in option.FileData) foreach (var (gamePath, fullPath) in option.FileData)
@ -192,16 +219,6 @@ public class ModNormalizer(ModManager _modManager, Configuration _config)
} }
} }
return true;
}
catch (Exception e)
{
Penumbra.Messager.NotificationMessage(e, $"Could not normalize mod {Mod.Name}.", NotificationType.Error, false);
}
return false;
}
private bool MoveOldFiles() private bool MoveOldFiles()
{ {
try try
@ -274,9 +291,20 @@ public class ModNormalizer(ModManager _modManager, Configuration _config)
private void ApplyRedirections() private void ApplyRedirections()
{ {
foreach (var option in Mod.AllSubMods) foreach (var (group, groupIdx) in Mod.Groups.WithIndex())
_modManager.OptionEditor.OptionSetFiles(Mod, option.GroupIdx, option.OptionIdx, {
_redirections[option.GroupIdx + 1][option.OptionIdx]); switch (group)
{
case SingleModGroup single:
foreach (var (_, optionIdx) in single.OptionData.WithIndex())
_modManager.OptionEditor.OptionSetFiles(Mod, groupIdx, optionIdx, _redirections[groupIdx + 1][optionIdx]);
break;
case MultiModGroup multi:
foreach (var (_, optionIdx) in multi.PrioritizedOptions.WithIndex())
_modManager.OptionEditor.OptionSetFiles(Mod, groupIdx, optionIdx, _redirections[groupIdx + 1][optionIdx]);
break;
}
}
++Step; ++Step;
} }

View file

@ -2,6 +2,7 @@ using Penumbra.Communication;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Subclasses;
using Penumbra.Services; using Penumbra.Services;
namespace Penumbra.Mods.Manager; namespace Penumbra.Mods.Manager;
@ -211,7 +212,14 @@ public class ModCacheManager : IDisposable
foreach (var group in mod.Groups) foreach (var group in mod.Groups)
{ {
mod.HasOptions |= group.IsOption; mod.HasOptions |= group.IsOption;
foreach (var s in group) var optionEnumerator = group switch
{
SingleModGroup single => single.OptionData,
MultiModGroup multi => multi.PrioritizedOptions.Select(o => o.Mod),
_ => [],
};
foreach (var s in optionEnumerator)
{ {
mod.TotalFileCount += s.Files.Count; mod.TotalFileCount += s.Files.Count;
mod.TotalSwapCount += s.FileSwaps.Count; mod.TotalSwapCount += s.FileSwaps.Count;

View file

@ -126,7 +126,7 @@ public static partial class ModMigration
case GroupType.Multi: case GroupType.Multi:
var optionPriority = ModPriority.Default; var optionPriority = ModPriority.Default;
var newMultiGroup = new MultiModGroup() var newMultiGroup = new MultiModGroup(mod)
{ {
Name = group.GroupName, Name = group.GroupName,
Priority = priority++, Priority = priority++,
@ -134,7 +134,7 @@ public static partial class ModMigration
}; };
mod.Groups.Add(newMultiGroup); mod.Groups.Add(newMultiGroup);
foreach (var option in group.Options) foreach (var option in group.Options)
newMultiGroup.PrioritizedOptions.Add((SubModFromOption(creator, mod, option, seenMetaFiles), optionPriority++)); newMultiGroup.PrioritizedOptions.Add((SubModFromOption(creator, mod, newMultiGroup, option, seenMetaFiles), optionPriority++));
break; break;
case GroupType.Single: case GroupType.Single:
@ -144,7 +144,7 @@ public static partial class ModMigration
return; return;
} }
var newSingleGroup = new SingleModGroup() var newSingleGroup = new SingleModGroup(mod)
{ {
Name = group.GroupName, Name = group.GroupName,
Priority = priority++, Priority = priority++,
@ -152,7 +152,7 @@ public static partial class ModMigration
}; };
mod.Groups.Add(newSingleGroup); mod.Groups.Add(newSingleGroup);
foreach (var option in group.Options) foreach (var option in group.Options)
newSingleGroup.OptionData.Add(SubModFromOption(creator, mod, option, seenMetaFiles)); newSingleGroup.OptionData.Add(SubModFromOption(creator, mod, newSingleGroup, option, seenMetaFiles));
break; break;
} }
@ -171,9 +171,9 @@ public static partial class ModMigration
} }
} }
private static SubMod SubModFromOption(ModCreator creator, Mod mod, OptionV0 option, HashSet<FullPath> seenMetaFiles) private static SubMod SubModFromOption(ModCreator creator, Mod mod, IModGroup group, OptionV0 option, HashSet<FullPath> seenMetaFiles)
{ {
var subMod = new SubMod(mod) { Name = option.OptionName }; var subMod = new SubMod(mod, group) { Name = option.OptionName };
AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles); AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles);
creator.IncorporateMetaChanges(subMod, mod.ModPath, false); creator.IncorporateMetaChanges(subMod, mod.ModPath, false);
return subMod; return subMod;

View file

@ -87,12 +87,12 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS
var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1; var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1;
mod.Groups.Add(type == GroupType.Multi mod.Groups.Add(type == GroupType.Multi
? new MultiModGroup ? new MultiModGroup(mod)
{ {
Name = newName, Name = newName,
Priority = maxPriority, Priority = maxPriority,
} }
: new SingleModGroup : new SingleModGroup(mod)
{ {
Name = newName, Name = newName,
Priority = maxPriority, Priority = maxPriority,
@ -120,7 +120,6 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS
{ {
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1); communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1);
mod.Groups.RemoveAt(groupIdx); mod.Groups.RemoveAt(groupIdx);
UpdateSubModPositions(mod, groupIdx);
saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport); saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport);
communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1); communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1);
} }
@ -131,7 +130,6 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS
if (!mod.Groups.Move(groupIdxFrom, groupIdxTo)) if (!mod.Groups.Move(groupIdxFrom, groupIdxTo))
return; return;
UpdateSubModPositions(mod, Math.Min(groupIdxFrom, groupIdxTo));
saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport); saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport);
communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo); communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo);
} }
@ -156,12 +154,9 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS
/// <summary> Change the description of the given option. </summary> /// <summary> Change the description of the given option. </summary>
public void ChangeOptionDescription(Mod mod, int groupIdx, int optionIdx, string newDescription) public void ChangeOptionDescription(Mod mod, int groupIdx, int optionIdx, string newDescription)
{ {
var group = mod.Groups[groupIdx]; if (!mod.Groups[groupIdx].ChangeOptionDescription(optionIdx, newDescription))
var option = group[optionIdx];
if (option.Description == newDescription)
return; return;
option.Description = newDescription;
saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1);
} }
@ -173,12 +168,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS
if (group.Priority == newPriority) if (group.Priority == newPriority)
return; return;
var _ = group switch group.Priority = newPriority;
{
SingleModGroup s => s.Priority = newPriority,
MultiModGroup m => m.Priority = newPriority,
_ => newPriority,
};
saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1); communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1);
} }
@ -188,14 +178,11 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS
{ {
switch (mod.Groups[groupIdx]) switch (mod.Groups[groupIdx])
{ {
case SingleModGroup: case MultiModGroup multi:
ChangeGroupPriority(mod, groupIdx, newPriority); if (multi.PrioritizedOptions[optionIdx].Priority == newPriority)
break;
case MultiModGroup m:
if (m.PrioritizedOptions[optionIdx].Priority == newPriority)
return; return;
m.PrioritizedOptions[optionIdx] = (m.PrioritizedOptions[optionIdx].Mod, newPriority); multi.PrioritizedOptions[optionIdx] = (multi.PrioritizedOptions[optionIdx].Mod, newPriority);
saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1); communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1);
return; return;
@ -205,60 +192,63 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS
/// <summary> Rename the given option. </summary> /// <summary> Rename the given option. </summary>
public void RenameOption(Mod mod, int groupIdx, int optionIdx, string newName) public void RenameOption(Mod mod, int groupIdx, int optionIdx, string newName)
{ {
switch (mod.Groups[groupIdx]) if (!mod.Groups[groupIdx].ChangeOptionName(optionIdx, newName))
{
case SingleModGroup s:
if (s.OptionData[optionIdx].Name == newName)
return; return;
s.OptionData[optionIdx].Name = newName;
break;
case MultiModGroup m:
var option = m.PrioritizedOptions[optionIdx].Mod;
if (option.Name == newName)
return;
option.Name = newName;
break;
}
saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1);
} }
/// <summary> Add a new empty option of the given name for the given group. </summary> /// <summary> Add a new empty option of the given name for the given group. </summary>
public void AddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue) public int AddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue)
{ {
var group = mod.Groups[groupIdx]; var group = mod.Groups[groupIdx];
var subMod = new SubMod(mod) { Name = newName }; var idx = group.AddOption(mod, newName);
subMod.SetPosition(groupIdx, group.Count); if (idx < 0)
switch (group) return -1;
{
case SingleModGroup s:
s.OptionData.Add(subMod);
break;
case MultiModGroup m:
m.PrioritizedOptions.Add((subMod, ModPriority.Default));
break;
}
saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1);
return idx;
} }
/// <summary> Add a new empty option of the given name for the given group if it does not exist already. </summary> /// <summary> Add a new empty option of the given name for the given group if it does not exist already. </summary>
public (SubMod, bool) FindOrAddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue) public (SubMod, int, bool) FindOrAddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue)
{ {
var group = mod.Groups[groupIdx]; var group = mod.Groups[groupIdx];
var idx = group.IndexOf(o => o.Name == newName); switch (group)
{
case SingleModGroup single:
{
var idx = single.OptionData.IndexOf(o => o.Name == newName);
if (idx >= 0) if (idx >= 0)
return ((SubMod)group[idx], false); return (single.OptionData[idx], idx, false);
AddOption(mod, groupIdx, newName, saveType); idx = single.AddOption(mod, newName);
if (group[^1].Name != newName) if (idx < 0)
throw new Exception($"Could not create new option with name {newName} in {group.Name}."); throw new Exception($"Could not create new option with name {newName} in {group.Name}.");
return ((SubMod)group[^1], true); saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1);
return (single.OptionData[^1], single.OptionData.Count - 1, true);
}
case MultiModGroup multi:
{
var idx = multi.PrioritizedOptions.IndexOf(o => o.Mod.Name == newName);
if (idx >= 0)
return (multi.PrioritizedOptions[idx].Mod, idx, false);
idx = multi.AddOption(mod, newName);
if (idx < 0)
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 (multi.PrioritizedOptions[^1].Mod, multi.PrioritizedOptions.Count - 1, true);
}
}
throw new Exception($"{nameof(FindOrAddOption)} is not supported for mod groups of type {group.GetType()}.");
} }
/// <summary> Add an existing option to a given group with default priority. </summary> /// <summary> Add an existing option to a given group with default priority. </summary>
@ -269,25 +259,28 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS
public void AddOption(Mod mod, int groupIdx, SubMod option, ModPriority priority) public void AddOption(Mod mod, int groupIdx, SubMod option, ModPriority priority)
{ {
var group = mod.Groups[groupIdx]; var group = mod.Groups[groupIdx];
int idx;
switch (group) switch (group)
{ {
case MultiModGroup { Count: >= IModGroup.MaxMultiOptions }: case MultiModGroup { PrioritizedOptions.Count: >= IModGroup.MaxMultiOptions }:
Penumbra.Log.Error( Penumbra.Log.Error(
$"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, " $"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."); + $"since only up to {IModGroup.MaxMultiOptions} options are supported in one group.");
return; return;
case SingleModGroup s: case SingleModGroup s:
option.SetPosition(groupIdx, s.Count); idx = s.OptionData.Count;
s.OptionData.Add(option); s.OptionData.Add(option);
break; break;
case MultiModGroup m: case MultiModGroup m:
option.SetPosition(groupIdx, m.Count); idx = m.PrioritizedOptions.Count;
m.PrioritizedOptions.Add((option, priority)); m.PrioritizedOptions.Add((option, priority));
break; break;
default:
return;
} }
saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1);
} }
/// <summary> Delete the given option from the given group. </summary> /// <summary> Delete the given option from the given group. </summary>
@ -306,7 +299,6 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS
break; break;
} }
group.UpdatePositions(optionIdx);
saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1); communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1);
} }
@ -396,16 +388,6 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS
return false; return false;
} }
/// <summary> Update the indices stored in options from a given group on. </summary>
private static void UpdateSubModPositions(Mod mod, int fromGroup)
{
foreach (var (group, groupIdx) in mod.Groups.WithIndex().Skip(fromGroup))
{
foreach (var (o, optionIdx) in group.OfType<SubMod>().WithIndex())
o.SetPosition(groupIdx, optionIdx);
}
}
/// <summary> Get the correct option for the given group and option index. </summary> /// <summary> Get the correct option for the given group and option index. </summary>
private static SubMod GetSubMod(Mod mod, int groupIdx, int optionIdx) private static SubMod GetSubMod(Mod mod, int groupIdx, int optionIdx)
{ {

View file

@ -38,7 +38,7 @@ public sealed class Mod : IMod
internal Mod(DirectoryInfo modPath) internal Mod(DirectoryInfo modPath)
{ {
ModPath = modPath; ModPath = modPath;
Default = new SubMod(this); Default = SubMod.CreateDefault(this);
} }
public override string ToString() public override string ToString()
@ -82,7 +82,12 @@ public sealed class Mod : IMod
} }
public IEnumerable<SubMod> AllSubMods public IEnumerable<SubMod> AllSubMods
=> Groups.SelectMany(o => o).Prepend(Default); => Groups.SelectMany(o => o switch
{
SingleModGroup single => single.OptionData,
MultiModGroup multi => multi.PrioritizedOptions.Select(s => s.Mod),
_ => [],
}).Prepend(Default);
public List<FullPath> FindUnusedFiles() public List<FullPath> FindUnusedFiles()
{ {

View file

@ -16,7 +16,11 @@ using Penumbra.String.Classes;
namespace Penumbra.Mods; namespace Penumbra.Mods;
public partial class ModCreator(SaveService _saveService, Configuration config, ModDataEditor _dataEditor, MetaFileManager _metaFileManager, public partial class ModCreator(
SaveService _saveService,
Configuration config,
ModDataEditor _dataEditor,
MetaFileManager _metaFileManager,
GamePathParser _gamePathParser) GamePathParser _gamePathParser)
{ {
public readonly Configuration Config = config; public readonly Configuration Config = config;
@ -106,7 +110,6 @@ public partial class ModCreator(SaveService _saveService, Configuration config,
public void LoadDefaultOption(Mod mod) public void LoadDefaultOption(Mod mod)
{ {
var defaultFile = _saveService.FileNames.OptionGroupFile(mod, -1, Config.ReplaceNonAsciiOnImport); var defaultFile = _saveService.FileNames.OptionGroupFile(mod, -1, Config.ReplaceNonAsciiOnImport);
mod.Default.SetPosition(-1, 0);
try try
{ {
if (!File.Exists(defaultFile)) if (!File.Exists(defaultFile))
@ -241,27 +244,21 @@ public partial class ModCreator(SaveService _saveService, Configuration config,
{ {
case GroupType.Multi: case GroupType.Multi:
{ {
var group = new MultiModGroup() var group = MultiModGroup.CreateForSaving(name);
{ group.Description = desc;
Name = name, group.Priority = priority;
Description = desc, group.DefaultSettings = defaultSettings;
Priority = priority,
DefaultSettings = defaultSettings,
};
group.PrioritizedOptions.AddRange(subMods.Select((s, idx) => (s, new ModPriority(idx)))); group.PrioritizedOptions.AddRange(subMods.Select((s, idx) => (s, new ModPriority(idx))));
_saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport));
break; break;
} }
case GroupType.Single: case GroupType.Single:
{ {
var group = new SingleModGroup() var group = SingleModGroup.CreateForSaving(name);
{ group.Description = desc;
Name = name, group.Priority = priority;
Description = desc, group.DefaultSettings = defaultSettings;
Priority = priority, group.OptionData.AddRange(subMods);
DefaultSettings = defaultSettings,
};
group.OptionData.AddRange(subMods.OfType<SubMod>());
_saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport));
break; break;
} }
@ -275,11 +272,8 @@ public partial class ModCreator(SaveService _saveService, Configuration config,
.Select(f => (Utf8GamePath.FromFile(f, optionFolder, out var gamePath, true), gamePath, new FullPath(f))) .Select(f => (Utf8GamePath.FromFile(f, optionFolder, out var gamePath, true), gamePath, new FullPath(f)))
.Where(t => t.Item1); .Where(t => t.Item1);
var mod = new SubMod(null!) // Mod is irrelevant here, only used for saving. var mod = SubMod.CreateForSaving(option.Name);
{ mod.Description = option.Description;
Name = option.Name,
Description = option.Description,
};
foreach (var (_, gamePath, file) in list) foreach (var (_, gamePath, file) in list)
mod.FileData.TryAdd(gamePath, file); mod.FileData.TryAdd(gamePath, file);
@ -287,13 +281,6 @@ public partial class ModCreator(SaveService _saveService, Configuration config,
return mod; return mod;
} }
/// <summary> Create an empty sub mod for single groups with None options. </summary>
internal static SubMod CreateEmptySubMod(string name)
=> new SubMod(null!) // Mod is irrelevant here, only used for saving.
{
Name = name,
};
/// <summary> /// <summary>
/// Create the default data file from all unused files that were not handled before /// Create the default data file from all unused files that were not handled before
/// and are used in sub mods. /// and are used in sub mods.

View file

@ -0,0 +1,80 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Penumbra.Meta.Manipulations;
using Penumbra.String.Classes;
namespace Penumbra.Mods.Subclasses;
public interface IModDataContainer
{
public Dictionary<Utf8GamePath, FullPath> Files { get; set; }
public Dictionary<Utf8GamePath, FullPath> FileSwaps { get; set; }
public HashSet<MetaManipulation> Manipulations { get; set; }
public void AddDataTo(Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations)
{
foreach (var (path, file) in Files)
redirections.TryAdd(path, file);
foreach (var (path, file) in FileSwaps)
redirections.TryAdd(path, file);
manipulations.UnionWith(Manipulations);
}
public static void Load(JToken json, IModDataContainer data, DirectoryInfo basePath)
{
data.Files.Clear();
data.FileSwaps.Clear();
data.Manipulations.Clear();
var files = (JObject?)json[nameof(Files)];
if (files != null)
foreach (var property in files.Properties())
{
if (Utf8GamePath.FromString(property.Name, out var p, true))
data.Files.TryAdd(p, new FullPath(basePath, property.Value.ToObject<Utf8RelPath>()));
}
var swaps = (JObject?)json[nameof(FileSwaps)];
if (swaps != null)
foreach (var property in swaps.Properties())
{
if (Utf8GamePath.FromString(property.Name, out var p, true))
data.FileSwaps.TryAdd(p, new FullPath(property.Value.ToObject<string>()!));
}
var manips = json[nameof(Manipulations)];
if (manips != null)
foreach (var s in manips.Children().Select(c => c.ToObject<MetaManipulation>())
.Where(m => m.Validate()))
data.Manipulations.Add(s);
}
public static void WriteModData(JsonWriter j, JsonSerializer serializer, IModDataContainer data, DirectoryInfo basePath)
{
j.WritePropertyName(nameof(data.Files));
j.WriteStartObject();
foreach (var (gamePath, file) in data.Files)
{
if (file.ToRelPath(basePath, out var relPath))
{
j.WritePropertyName(gamePath.ToString());
j.WriteValue(relPath.ToString());
}
}
j.WriteEndObject();
j.WritePropertyName(nameof(data.FileSwaps));
j.WriteStartObject();
foreach (var (gamePath, file) in data.FileSwaps)
{
j.WritePropertyName(gamePath.ToString());
j.WriteValue(file.ToString());
}
j.WriteEndObject();
j.WritePropertyName(nameof(data.Manipulations));
serializer.Serialize(j, data.Manipulations);
j.WriteEndObject();
}
}

View file

@ -6,25 +6,27 @@ using Penumbra.String.Classes;
namespace Penumbra.Mods.Subclasses; namespace Penumbra.Mods.Subclasses;
public interface IModGroup : IReadOnlyCollection<SubMod> public interface IModGroup
{ {
public const int MaxMultiOptions = 63; public const int MaxMultiOptions = 63;
public Mod Mod { get; }
public string Name { get; } public string Name { get; }
public string Description { get; } public string Description { get; }
public GroupType Type { get; } public GroupType Type { get; }
public ModPriority Priority { get; } public ModPriority Priority { get; set; }
public Setting DefaultSettings { get; set; } public Setting DefaultSettings { get; set; }
public FullPath? FindBestMatch(Utf8GamePath gamePath); public FullPath? FindBestMatch(Utf8GamePath gamePath);
public int AddOption(Mod mod, string name, string description = "");
public bool ChangeOptionDescription(int optionIndex, string newDescription);
public bool ChangeOptionName(int optionIndex, string newName);
public SubMod this[Index idx] { get; } public IReadOnlyList<IModOption> Options { get; }
public bool IsOption { get; } public bool IsOption { get; }
public IModGroup Convert(GroupType type); public IModGroup Convert(GroupType type);
public bool MoveOption(int optionIdxFrom, int optionIdxTo); public bool MoveOption(int optionIdxFrom, int optionIdxTo);
public void UpdatePositions(int from = 0);
public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations); public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations);

View file

@ -0,0 +1,25 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Penumbra.Mods.Subclasses;
public interface IModOption
{
public string Name { get; set; }
public string FullName { get; }
public string Description { get; set; }
public static void Load(JToken json, IModOption option)
{
option.Name = json[nameof(Name)]?.ToObject<string>() ?? string.Empty;
option.Description = json[nameof(Description)]?.ToObject<string>() ?? string.Empty;
}
public static void WriteModOption(JsonWriter j, IModOption option)
{
j.WritePropertyName(nameof(Name));
j.WriteValue(option.Name);
j.WritePropertyName(nameof(Description));
j.WriteValue(option.Description);
}
}

View file

@ -68,7 +68,7 @@ public class ModSettings
var config = Settings[groupIdx]; var config = Settings[groupIdx];
Settings[groupIdx] = group.Type switch Settings[groupIdx] = group.Type switch
{ {
GroupType.Single => config.TurnMulti(group.Count), GroupType.Single => config.TurnMulti(group.Options.Count),
GroupType.Multi => Setting.Multi((int)config.Value), GroupType.Multi => Setting.Multi((int)config.Value),
_ => config, _ => config,
}; };
@ -182,15 +182,15 @@ public class ModSettings
if (idx >= mod.Groups.Count) if (idx >= mod.Groups.Count)
break; break;
var group = mod.Groups[idx]; switch (mod.Groups[idx])
if (group.Type == GroupType.Single && setting.Value < (ulong)group.Count)
{ {
dict.Add(group.Name, [group[(int)setting.Value].Name]); case SingleModGroup single when setting.Value < (ulong)single.Options.Count:
} dict.Add(single.Name, [single.Options[setting.AsIndex].Name]);
else break;
{ case MultiModGroup multi:
var list = group.Where((_, optionIdx) => (setting.Value & (1ul << optionIdx)) != 0).Select(o => o.Name).ToList(); var list = multi.Options.WithIndex().Where(p => setting.HasFlag(p.Index)).Select(p => p.Value.Name).ToList();
dict.Add(group.Name, list); dict.Add(multi.Name, list);
break;
} }
} }

View file

@ -1,5 +1,4 @@
using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.Notifications;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using OtterGui; using OtterGui;
using OtterGui.Classes; using OtterGui.Classes;
@ -11,11 +10,12 @@ using Penumbra.String.Classes;
namespace Penumbra.Mods.Subclasses; namespace Penumbra.Mods.Subclasses;
/// <summary> Groups that allow all available options to be selected at once. </summary> /// <summary> Groups that allow all available options to be selected at once. </summary>
public sealed class MultiModGroup : IModGroup public sealed class MultiModGroup(Mod mod) : IModGroup
{ {
public GroupType Type public GroupType Type
=> GroupType.Multi; => GroupType.Multi;
public Mod Mod { get; set; } = mod;
public string Name { get; set; } = "Group"; public string Name { get; set; } = "Group";
public string Description { get; set; } = "A non-exclusive group of settings."; public string Description { get; set; } = "A non-exclusive group of settings.";
public ModPriority Priority { get; set; } public ModPriority Priority { get; set; }
@ -26,27 +26,58 @@ public sealed class MultiModGroup : IModGroup
.SelectWhere(o => (o.Mod.FileData.TryGetValue(gamePath, out var file) || o.Mod.FileSwapData.TryGetValue(gamePath, out file), file)) .SelectWhere(o => (o.Mod.FileData.TryGetValue(gamePath, out var file) || o.Mod.FileSwapData.TryGetValue(gamePath, out file), file))
.FirstOrDefault(); .FirstOrDefault();
public SubMod this[Index idx] public int AddOption(Mod mod, string name, string description = "")
=> PrioritizedOptions[idx].Mod; {
var groupIdx = mod.Groups.IndexOf(this);
if (groupIdx < 0)
return -1;
var subMod = new SubMod(mod, this)
{
Name = name,
Description = description,
};
PrioritizedOptions.Add((subMod, ModPriority.Default));
return PrioritizedOptions.Count - 1;
}
public bool ChangeOptionDescription(int optionIndex, string newDescription)
{
if (optionIndex < 0 || optionIndex >= PrioritizedOptions.Count)
return false;
var option = PrioritizedOptions[optionIndex].Mod;
if (option.Description == newDescription)
return false;
option.Description = newDescription;
return true;
}
public bool ChangeOptionName(int optionIndex, string newName)
{
if (optionIndex < 0 || optionIndex >= PrioritizedOptions.Count)
return false;
var option = PrioritizedOptions[optionIndex].Mod;
if (option.Name == newName)
return false;
option.Name = newName;
return true;
}
public IReadOnlyList<IModOption> Options
=> PrioritizedOptions.Select(p => p.Mod).ToArray();
public bool IsOption public bool IsOption
=> Count > 0; => PrioritizedOptions.Count > 0;
[JsonIgnore]
public int Count
=> PrioritizedOptions.Count;
public readonly List<(SubMod Mod, ModPriority Priority)> PrioritizedOptions = []; public readonly List<(SubMod Mod, ModPriority Priority)> PrioritizedOptions = [];
public IEnumerator<SubMod> GetEnumerator()
=> PrioritizedOptions.Select(o => o.Mod).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public static MultiModGroup? Load(Mod mod, JObject json, int groupIdx) public static MultiModGroup? Load(Mod mod, JObject json, int groupIdx)
{ {
var ret = new MultiModGroup() var ret = new MultiModGroup(mod)
{ {
Name = json[nameof(Name)]?.ToObject<string>() ?? string.Empty, Name = json[nameof(Name)]?.ToObject<string>() ?? string.Empty,
Description = json[nameof(Description)]?.ToObject<string>() ?? string.Empty, Description = json[nameof(Description)]?.ToObject<string>() ?? string.Empty,
@ -68,8 +99,7 @@ public sealed class MultiModGroup : IModGroup
break; break;
} }
var subMod = new SubMod(mod); var subMod = new SubMod(mod, ret);
subMod.SetPosition(groupIdx, ret.PrioritizedOptions.Count);
subMod.Load(mod.ModPath, child, out var priority); subMod.Load(mod.ModPath, child, out var priority);
ret.PrioritizedOptions.Add((subMod, priority)); ret.PrioritizedOptions.Add((subMod, priority));
} }
@ -85,12 +115,12 @@ public sealed class MultiModGroup : IModGroup
{ {
case GroupType.Multi: return this; case GroupType.Multi: return this;
case GroupType.Single: case GroupType.Single:
var multi = new SingleModGroup() var multi = new SingleModGroup(Mod)
{ {
Name = Name, Name = Name,
Description = Description, Description = Description,
Priority = Priority, Priority = Priority,
DefaultSettings = DefaultSettings.TurnMulti(Count), DefaultSettings = DefaultSettings.TurnMulti(PrioritizedOptions.Count),
}; };
multi.OptionData.AddRange(PrioritizedOptions.Select(p => p.Mod)); multi.OptionData.AddRange(PrioritizedOptions.Select(p => p.Mod));
return multi; return multi;
@ -104,16 +134,9 @@ public sealed class MultiModGroup : IModGroup
return false; return false;
DefaultSettings = DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo); DefaultSettings = DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo);
UpdatePositions(Math.Min(optionIdxFrom, optionIdxTo));
return true; return true;
} }
public void UpdatePositions(int from = 0)
{
foreach (var ((o, _), i) in PrioritizedOptions.WithIndex().Skip(from))
o.SetPosition(o.GroupIdx, i);
}
public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations) public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations)
{ {
foreach (var (option, index) in PrioritizedOptions.WithIndex().OrderByDescending(o => o.Value.Priority)) foreach (var (option, index) in PrioritizedOptions.WithIndex().OrderByDescending(o => o.Value.Priority))
@ -124,5 +147,12 @@ public sealed class MultiModGroup : IModGroup
} }
public Setting FixSetting(Setting setting) public Setting FixSetting(Setting setting)
=> new(setting.Value & ((1ul << Count) - 1)); => new(setting.Value & ((1ul << PrioritizedOptions.Count) - 1));
/// <summary> Create a group without a mod only for saving it in the creator. </summary>
internal static MultiModGroup CreateForSaving(string name)
=> new(null!)
{
Name = name,
};
} }

View file

@ -1,4 +1,3 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using OtterGui; using OtterGui;
using OtterGui.Filesystem; using OtterGui.Filesystem;
@ -9,11 +8,12 @@ using Penumbra.String.Classes;
namespace Penumbra.Mods.Subclasses; namespace Penumbra.Mods.Subclasses;
/// <summary> Groups that allow only one of their available options to be selected. </summary> /// <summary> Groups that allow only one of their available options to be selected. </summary>
public sealed class SingleModGroup : IModGroup public sealed class SingleModGroup(Mod mod) : IModGroup
{ {
public GroupType Type public GroupType Type
=> GroupType.Single; => GroupType.Single;
public Mod Mod { get; set; } = mod;
public string Name { get; set; } = "Option"; public string Name { get; set; } = "Option";
public string Description { get; set; } = "A mutually exclusive group of settings."; public string Description { get; set; } = "A mutually exclusive group of settings.";
public ModPriority Priority { get; set; } public ModPriority Priority { get; set; }
@ -26,26 +26,53 @@ public sealed class SingleModGroup : IModGroup
.SelectWhere(m => (m.FileData.TryGetValue(gamePath, out var file) || m.FileSwapData.TryGetValue(gamePath, out file), file)) .SelectWhere(m => (m.FileData.TryGetValue(gamePath, out var file) || m.FileSwapData.TryGetValue(gamePath, out file), file))
.FirstOrDefault(); .FirstOrDefault();
public SubMod this[Index idx] public int AddOption(Mod mod, string name, string description = "")
=> OptionData[idx]; {
var subMod = new SubMod(mod, this)
{
Name = name,
Description = description,
};
OptionData.Add(subMod);
return OptionData.Count - 1;
}
public bool ChangeOptionDescription(int optionIndex, string newDescription)
{
if (optionIndex < 0 || optionIndex >= OptionData.Count)
return false;
var option = OptionData[optionIndex];
if (option.Description == newDescription)
return false;
option.Description = newDescription;
return true;
}
public bool ChangeOptionName(int optionIndex, string newName)
{
if (optionIndex < 0 || optionIndex >= OptionData.Count)
return false;
var option = OptionData[optionIndex];
if (option.Name == newName)
return false;
option.Name = newName;
return true;
}
public IReadOnlyList<IModOption> Options
=> OptionData;
public bool IsOption public bool IsOption
=> Count > 1; => OptionData.Count > 1;
[JsonIgnore]
public int Count
=> OptionData.Count;
public IEnumerator<SubMod> GetEnumerator()
=> OptionData.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public static SingleModGroup? Load(Mod mod, JObject json, int groupIdx) public static SingleModGroup? Load(Mod mod, JObject json, int groupIdx)
{ {
var options = json["Options"]; var options = json["Options"];
var ret = new SingleModGroup var ret = new SingleModGroup(mod)
{ {
Name = json[nameof(Name)]?.ToObject<string>() ?? string.Empty, Name = json[nameof(Name)]?.ToObject<string>() ?? string.Empty,
Description = json[nameof(Description)]?.ToObject<string>() ?? string.Empty, Description = json[nameof(Description)]?.ToObject<string>() ?? string.Empty,
@ -58,8 +85,7 @@ public sealed class SingleModGroup : IModGroup
if (options != null) if (options != null)
foreach (var child in options.Children()) foreach (var child in options.Children())
{ {
var subMod = new SubMod(mod); var subMod = new SubMod(mod, ret);
subMod.SetPosition(groupIdx, ret.OptionData.Count);
subMod.Load(mod.ModPath, child, out _); subMod.Load(mod.ModPath, child, out _);
ret.OptionData.Add(subMod); ret.OptionData.Add(subMod);
} }
@ -74,7 +100,7 @@ public sealed class SingleModGroup : IModGroup
{ {
case GroupType.Single: return this; case GroupType.Single: return this;
case GroupType.Multi: case GroupType.Multi:
var multi = new MultiModGroup() var multi = new MultiModGroup(Mod)
{ {
Name = Name, Name = Name,
Description = Description, Description = Description,
@ -108,19 +134,19 @@ public sealed class SingleModGroup : IModGroup
DefaultSettings = Setting.Single(currentIndex + 1); DefaultSettings = Setting.Single(currentIndex + 1);
} }
UpdatePositions(Math.Min(optionIdxFrom, optionIdxTo));
return true; return true;
} }
public void UpdatePositions(int from = 0)
{
foreach (var (o, i) in OptionData.WithIndex().Skip(from))
o.SetPosition(o.GroupIdx, i);
}
public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations) public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations)
=> this[setting.AsIndex].AddData(redirections, manipulations); => OptionData[setting.AsIndex].AddData(redirections, manipulations);
public Setting FixSetting(Setting setting) public Setting FixSetting(Setting setting)
=> Count == 0 ? Setting.Zero : new Setting(Math.Min(setting.Value, (ulong)(Count - 1))); => OptionData.Count == 0 ? Setting.Zero : new Setting(Math.Min(setting.Value, (ulong)(OptionData.Count - 1)));
/// <summary> Create a group without a mod only for saving it in the creator. </summary>
internal static SingleModGroup CreateForSaving(string name)
=> new(null!)
{
Name = name,
};
} }

View file

@ -1,11 +1,62 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using OtterGui;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor; using Penumbra.Mods.Editor;
using Penumbra.String.Classes; using Penumbra.String.Classes;
namespace Penumbra.Mods.Subclasses; namespace Penumbra.Mods.Subclasses;
public class SingleSubMod(Mod mod, SingleModGroup group) : IModOption, IModDataContainer
{
internal readonly Mod Mod = mod;
internal readonly SingleModGroup Group = group;
public string Name { get; set; } = "Option";
public string FullName
=> $"{Group.Name}: {Name}";
public string Description { get; set; } = string.Empty;
public Dictionary<Utf8GamePath, FullPath> Files { get; set; } = [];
public Dictionary<Utf8GamePath, FullPath> FileSwaps { get; set; } = [];
public HashSet<MetaManipulation> Manipulations { get; set; } = [];
}
public class MultiSubMod(Mod mod, MultiModGroup group) : IModOption, IModDataContainer
{
internal readonly Mod Mod = mod;
internal readonly MultiModGroup Group = group;
public string Name { get; set; } = "Option";
public string FullName
=> $"{Group.Name}: {Name}";
public string Description { get; set; } = string.Empty;
public ModPriority Priority { get; set; } = ModPriority.Default;
public Dictionary<Utf8GamePath, FullPath> Files { get; set; } = [];
public Dictionary<Utf8GamePath, FullPath> FileSwaps { get; set; } = [];
public HashSet<MetaManipulation> Manipulations { get; set; } = [];
}
public class DefaultSubMod(IMod mod) : IModDataContainer
{
public string FullName
=> "Default Option";
public string Description
=> string.Empty;
internal readonly IMod Mod = mod;
public Dictionary<Utf8GamePath, FullPath> Files { get; set; } = [];
public Dictionary<Utf8GamePath, FullPath> FileSwaps { get; set; } = [];
public HashSet<MetaManipulation> Manipulations { get; set; } = [];
}
/// <summary> /// <summary>
/// A sub mod is a collection of /// A sub mod is a collection of
/// - file replacements /// - file replacements
@ -16,21 +67,51 @@ namespace Penumbra.Mods.Subclasses;
/// Nothing is checked for existence or validity when loading. /// Nothing is checked for existence or validity when loading.
/// Objects are also not checked for uniqueness, the first appearance of a game path or meta path decides. /// Objects are also not checked for uniqueness, the first appearance of a game path or meta path decides.
/// </summary> /// </summary>
public sealed class SubMod public sealed class SubMod(IMod mod, IModGroup group) : IModOption
{ {
public string Name { get; set; } = "Default"; public string Name { get; set; } = "Default";
public string FullName public string FullName
=> GroupIdx < 0 ? "Default Option" : $"{ParentMod.Groups[GroupIdx].Name}: {Name}"; => Group == null ? "Default Option" : $"{Group.Name}: {Name}";
public string Description { get; set; } = string.Empty; public string Description { get; set; } = string.Empty;
internal IMod ParentMod { get; private init; } internal readonly IMod Mod = mod;
internal int GroupIdx { get; private set; } internal readonly IModGroup? Group = group;
internal int OptionIdx { get; private set; }
internal (int GroupIdx, int OptionIdx) GetIndices()
{
if (IsDefault)
return (-1, 0);
var groupIdx = Mod.Groups.IndexOf(Group);
if (groupIdx < 0)
throw new Exception($"Group {Group.Name} from SubMod {Name} is not contained in Mod {Mod.Name}.");
return (groupIdx, GetOptionIndex());
}
private int GetOptionIndex()
{
var optionIndex = Group switch
{
null => 0,
SingleModGroup single => single.OptionData.IndexOf(this),
MultiModGroup multi => multi.PrioritizedOptions.IndexOf(p => p.Mod == this),
_ => throw new Exception($"Group {Group.Name} from SubMod {Name} has unknown type {typeof(Group)}"),
};
if (optionIndex < 0)
throw new Exception($"Group {Group!.Name} from SubMod {Name} does not contain this SubMod.");
return optionIndex;
}
public static SubMod CreateDefault(IMod mod)
=> new(mod, null!);
[MemberNotNullWhen(false, nameof(Group))]
public bool IsDefault public bool IsDefault
=> GroupIdx < 0; => Group == null;
public void AddData(Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations) public void AddData(Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations)
{ {
@ -46,9 +127,6 @@ public sealed class SubMod
public Dictionary<Utf8GamePath, FullPath> FileSwapData = []; public Dictionary<Utf8GamePath, FullPath> FileSwapData = [];
public HashSet<MetaManipulation> ManipulationData = []; public HashSet<MetaManipulation> ManipulationData = [];
public SubMod(IMod parentMod)
=> ParentMod = parentMod;
public IReadOnlyDictionary<Utf8GamePath, FullPath> Files public IReadOnlyDictionary<Utf8GamePath, FullPath> Files
=> FileData; => FileData;
@ -58,12 +136,6 @@ public sealed class SubMod
public IReadOnlySet<MetaManipulation> Manipulations public IReadOnlySet<MetaManipulation> Manipulations
=> ManipulationData; => ManipulationData;
public void SetPosition(int groupIdx, int optionIdx)
{
GroupIdx = groupIdx;
OptionIdx = optionIdx;
}
public void Load(DirectoryInfo basePath, JToken json, out ModPriority priority) public void Load(DirectoryInfo basePath, JToken json, out ModPriority priority)
{ {
FileData.Clear(); FileData.Clear();
@ -116,6 +188,14 @@ public sealed class SubMod
} }
} }
/// <summary> Create a sub mod without a mod or group only for saving it in the creator. </summary>
internal static SubMod CreateForSaving(string name)
=> new(null!, null!)
{
Name = name,
};
public static void WriteSubMod(JsonWriter j, JsonSerializer serializer, SubMod mod, DirectoryInfo basePath, ModPriority? priority) public static void WriteSubMod(JsonWriter j, JsonSerializer serializer, SubMod mod, DirectoryInfo basePath, ModPriority? priority)
{ {
j.WriteStartObject(); j.WriteStartObject();

View file

@ -49,7 +49,7 @@ public class TemporaryMod : IMod
=> [Default]; => [Default];
public TemporaryMod() public TemporaryMod()
=> Default = new SubMod(this); => Default = SubMod.CreateDefault(this);
public void SetFile(Utf8GamePath gamePath, FullPath fullPath) public void SetFile(Utf8GamePath gamePath, FullPath fullPath)
=> Default.FileData[gamePath] = fullPath; => Default.FileData[gamePath] = fullPath;

View file

@ -253,7 +253,7 @@ public class ItemSwapTab : IDisposable, ITab
_subModValid = _mod != null _subModValid = _mod != null
&& _newGroupName.Length > 0 && _newGroupName.Length > 0
&& _newOptionName.Length > 0 && _newOptionName.Length > 0
&& (_selectedGroup?.All(o => o.Name != _newOptionName) ?? true); && (_selectedGroup?.Options.All(o => o.Name != _newOptionName) ?? true);
} }
private void CreateMod() private void CreateMod()
@ -275,7 +275,7 @@ public class ItemSwapTab : IDisposable, ITab
var groupCreated = false; var groupCreated = false;
var dirCreated = false; var dirCreated = false;
var optionCreated = false; var optionCreated = -1;
DirectoryInfo? optionFolderName = null; DirectoryInfo? optionFolderName = null;
try try
{ {
@ -294,14 +294,17 @@ public class ItemSwapTab : IDisposable, ITab
groupCreated = true; groupCreated = true;
} }
_modManager.OptionEditor.AddOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _newOptionName); var optionIdx = _modManager.OptionEditor.AddOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _newOptionName);
optionCreated = true; if (optionIdx < 0)
throw new Exception($"Failure creating mod option.");
optionCreated = optionIdx;
optionFolderName = Directory.CreateDirectory(optionFolderName.FullName); optionFolderName = Directory.CreateDirectory(optionFolderName.FullName);
dirCreated = true; dirCreated = true;
if (!_swapData.WriteMod(_modManager, _mod, if (!_swapData.WriteMod(_modManager, _mod,
_useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps, _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps,
optionFolderName, optionFolderName,
_mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1)) _mod.Groups.IndexOf(_selectedGroup), optionIdx))
throw new Exception("Failure writing files for mod swap."); throw new Exception("Failure writing files for mod swap.");
} }
} }
@ -310,8 +313,8 @@ public class ItemSwapTab : IDisposable, ITab
Penumbra.Messager.NotificationMessage(e, "Could not create new Swap Option.", NotificationType.Error, false); Penumbra.Messager.NotificationMessage(e, "Could not create new Swap Option.", NotificationType.Error, false);
try try
{ {
if (optionCreated && _selectedGroup != null) if (optionCreated >= 0 && _selectedGroup != null)
_modManager.OptionEditor.DeleteOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1); _modManager.OptionEditor.DeleteOption(_mod, _mod.Groups.IndexOf(_selectedGroup), optionCreated);
if (groupCreated) if (groupCreated)
{ {

View file

@ -78,7 +78,10 @@ public partial class ModEditWindow : Window, IDisposable
} }
public void ChangeOption(SubMod? subMod) public void ChangeOption(SubMod? subMod)
=> _editor.LoadOption(subMod?.GroupIdx ?? -1, subMod?.OptionIdx ?? 0); {
var (groupIdx, optionIdx) = subMod?.GetIndices() ?? (-1, 0);
_editor.LoadOption(groupIdx, optionIdx);
}
public void UpdateModels() public void UpdateModels()
{ {
@ -428,7 +431,8 @@ public partial class ModEditWindow : Window, IDisposable
using var id = ImRaii.PushId(idx); using var id = ImRaii.PushId(idx);
if (ImGui.Selectable(option.FullName, option == _editor.Option)) if (ImGui.Selectable(option.FullName, option == _editor.Option))
{ {
_editor.LoadOption(option.GroupIdx, option.OptionIdx); var (groupIdx, optionIdx) = option.GetIndices();
_editor.LoadOption(groupIdx, optionIdx);
ret = true; ret = true;
} }
} }
@ -565,7 +569,7 @@ public partial class ModEditWindow : Window, IDisposable
} }
if (Mod != null) if (Mod != null)
foreach (var option in Mod.Groups.SelectMany(g => g).Append(Mod.Default)) foreach (var option in Mod.AllSubMods)
{ {
foreach (var path in option.Files.Keys) foreach (var path in option.Files.Keys)
{ {

View file

@ -71,7 +71,7 @@ public class ModMergeTab(ModMerger modMerger)
color = color == Colors.DiscordColor color = color == Colors.DiscordColor
? Colors.DiscordColor ? Colors.DiscordColor
: group == null || group.Any(o => o.Name == modMerger.OptionName) : group == null || group.Options.Any(o => o.Name == modMerger.OptionName)
? Colors.PressEnterWarningBg ? Colors.PressEnterWarningBg
: Colors.DiscordColor; : Colors.DiscordColor;
c.Push(ImGuiCol.Border, color); c.Push(ImGuiCol.Border, color);
@ -184,18 +184,26 @@ public class ModMergeTab(ModMerger modMerger)
else else
{ {
ImGuiUtil.DrawTableColumn(option.Name); ImGuiUtil.DrawTableColumn(option.Name);
var group = option.ParentMod.Groups[option.GroupIdx]; var group = option.Group;
var optionEnumerator = group switch
{
SingleModGroup single => single.OptionData,
MultiModGroup multi => multi.PrioritizedOptions.Select(o => o.Mod),
_ => [],
};
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.Selectable(group.Name, false); ImGui.Selectable(group.Name, false);
if (ImGui.BeginPopupContextItem("##groupContext")) if (ImGui.BeginPopupContextItem("##groupContext"))
{ {
if (ImGui.MenuItem("Select All")) if (ImGui.MenuItem("Select All"))
foreach (var opt in group) // ReSharper disable once PossibleMultipleEnumeration
Handle((SubMod)opt, true); foreach (var opt in optionEnumerator)
Handle(opt, true);
if (ImGui.MenuItem("Unselect All")) if (ImGui.MenuItem("Unselect All"))
foreach (var opt in group) // ReSharper disable once PossibleMultipleEnumeration
Handle((SubMod)opt, false); foreach (var opt in optionEnumerator)
Handle(opt, false);
ImGui.EndPopup(); ImGui.EndPopup();
} }
} }

View file

@ -324,7 +324,7 @@ public class ModPanelEditTab(
? mod.Description ? mod.Description
: optionIdx < 0 : optionIdx < 0
? mod.Groups[groupIdx].Description ? mod.Groups[groupIdx].Description
: mod.Groups[groupIdx][optionIdx].Description; : mod.Groups[groupIdx].Options[optionIdx].Description;
_oldDescription = _newDescription; _oldDescription = _newDescription;
_mod = mod; _mod = mod;
@ -479,17 +479,24 @@ public class ModPanelEditTab(
ImGui.TableSetupColumn("delete", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X); ImGui.TableSetupColumn("delete", ImGuiTableColumnFlags.WidthFixed, UiHelpers.IconButtonSize.X);
ImGui.TableSetupColumn("priority", ImGuiTableColumnFlags.WidthFixed, 50 * UiHelpers.Scale); ImGui.TableSetupColumn("priority", ImGuiTableColumnFlags.WidthFixed, 50 * UiHelpers.Scale);
var group = panel._mod.Groups[groupIdx]; switch (panel._mod.Groups[groupIdx])
for (var optionIdx = 0; optionIdx < group.Count; ++optionIdx) {
EditOption(panel, group, groupIdx, optionIdx); case SingleModGroup single:
for (var optionIdx = 0; optionIdx < single.OptionData.Count; ++optionIdx)
EditOption(panel, single, groupIdx, optionIdx);
break;
case MultiModGroup multi:
for (var optionIdx = 0; optionIdx < multi.PrioritizedOptions.Count; ++optionIdx)
EditOption(panel, multi, groupIdx, optionIdx);
break;
}
DrawNewOption(panel, groupIdx, UiHelpers.IconButtonSize); DrawNewOption(panel, groupIdx, UiHelpers.IconButtonSize);
} }
/// <summary> Draw a line for a single option. </summary> /// <summary> Draw a line for a single option. </summary>
private static void EditOption(ModPanelEditTab panel, IModGroup group, int groupIdx, int optionIdx) private static void EditOption(ModPanelEditTab panel, IModGroup group, int groupIdx, int optionIdx)
{ {
var option = group[optionIdx]; var option = group.Options[optionIdx];
using var id = ImRaii.PushId(optionIdx); using var id = ImRaii.PushId(optionIdx);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
@ -547,10 +554,16 @@ public class ModPanelEditTab(
{ {
var mod = panel._mod; var mod = panel._mod;
var group = mod.Groups[groupIdx]; var group = mod.Groups[groupIdx];
var count = group switch
{
SingleModGroup single => single.OptionData.Count,
MultiModGroup multi => multi.PrioritizedOptions.Count,
_ => throw new Exception($"Dragging options to an option group of type {group.GetType()} is not supported."),
};
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
ImGui.Selectable($"Option #{group.Count + 1}"); ImGui.Selectable($"Option #{count + 1}");
Target(panel, group, groupIdx, group.Count); Target(panel, group, groupIdx, count);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.SetNextItemWidth(-1); ImGui.SetNextItemWidth(-1);
@ -562,7 +575,7 @@ public class ModPanelEditTab(
} }
ImGui.TableNextColumn(); ImGui.TableNextColumn();
var canAddGroup = mod.Groups[groupIdx].Type != GroupType.Multi || mod.Groups[groupIdx].Count < IModGroup.MaxMultiOptions; var canAddGroup = mod.Groups[groupIdx].Type != GroupType.Multi || count < IModGroup.MaxMultiOptions;
var validName = _newOptionName.Length > 0 && _newOptionNameIdx == groupIdx; var validName = _newOptionName.Length > 0 && _newOptionNameIdx == groupIdx;
var tt = canAddGroup var tt = canAddGroup
? validName ? "Add a new option to this group." : "Please enter a name for the new option." ? validName ? "Add a new option to this group." : "Please enter a name for the new option."
@ -588,7 +601,7 @@ public class ModPanelEditTab(
_dragDropOptionIdx = optionIdx; _dragDropOptionIdx = optionIdx;
} }
ImGui.TextUnformatted($"Dragging option {group[optionIdx].Name} from group {group.Name}..."); ImGui.TextUnformatted($"Dragging option {group.Options[optionIdx].Name} from group {group.Name}...");
} }
private static void Target(ModPanelEditTab panel, IModGroup group, int groupIdx, int optionIdx) private static void Target(ModPanelEditTab panel, IModGroup group, int groupIdx, int optionIdx)
@ -611,12 +624,17 @@ public class ModPanelEditTab(
var sourceGroupIdx = _dragDropGroupIdx; var sourceGroupIdx = _dragDropGroupIdx;
var sourceOption = _dragDropOptionIdx; var sourceOption = _dragDropOptionIdx;
var sourceGroup = panel._mod.Groups[sourceGroupIdx]; var sourceGroup = panel._mod.Groups[sourceGroupIdx];
var currentCount = group.Count; var currentCount = group switch
var option = sourceGroup[sourceOption];
var priority = sourceGroup switch
{ {
MultiModGroup multi => multi.PrioritizedOptions[_dragDropOptionIdx].Priority, SingleModGroup single => single.OptionData.Count,
_ => ModPriority.Default, MultiModGroup multi => multi.PrioritizedOptions.Count,
_ => throw new Exception($"Dragging options to an option group of type {group.GetType()} is not supported."),
};
var (option, priority) = sourceGroup switch
{
SingleModGroup single => (single.OptionData[_dragDropOptionIdx], ModPriority.Default),
MultiModGroup multi => multi.PrioritizedOptions[_dragDropOptionIdx],
_ => throw new Exception($"Dragging options from an option group of type {sourceGroup.GetType()} is not supported."),
}; };
panel._delayedActions.Enqueue(() => panel._delayedActions.Enqueue(() =>
{ {
@ -651,7 +669,7 @@ public class ModPanelEditTab(
if (ImGui.Selectable(GroupTypeName(GroupType.Single), group.Type == GroupType.Single)) if (ImGui.Selectable(GroupTypeName(GroupType.Single), group.Type == GroupType.Single))
_modManager.OptionEditor.ChangeModGroupType(_mod, groupIdx, GroupType.Single); _modManager.OptionEditor.ChangeModGroupType(_mod, groupIdx, GroupType.Single);
var canSwitchToMulti = group.Count <= IModGroup.MaxMultiOptions; var canSwitchToMulti = group.Options.Count <= IModGroup.MaxMultiOptions;
using var style = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, !canSwitchToMulti); using var style = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, !canSwitchToMulti);
if (ImGui.Selectable(GroupTypeName(GroupType.Multi), group.Type == GroupType.Multi) && canSwitchToMulti) if (ImGui.Selectable(GroupTypeName(GroupType.Multi), group.Type == GroupType.Multi) && canSwitchToMulti)
_modManager.OptionEditor.ChangeModGroupType(_mod, groupIdx, GroupType.Multi); _modManager.OptionEditor.ChangeModGroupType(_mod, groupIdx, GroupType.Multi);

View file

@ -75,7 +75,7 @@ public class ModPanelSettingsTab : ITab
{ {
var useDummy = true; var useDummy = true;
foreach (var (group, idx) in _selector.Selected!.Groups.WithIndex() foreach (var (group, idx) in _selector.Selected!.Groups.WithIndex()
.Where(g => g.Value.Type == GroupType.Single && g.Value.Count > _config.SingleGroupRadioMax)) .Where(g => g.Value.Type == GroupType.Single && g.Value.Options.Count > _config.SingleGroupRadioMax))
{ {
ImGuiUtil.Dummy(UiHelpers.DefaultSpace, useDummy); ImGuiUtil.Dummy(UiHelpers.DefaultSpace, useDummy);
useDummy = false; useDummy = false;
@ -92,7 +92,7 @@ public class ModPanelSettingsTab : ITab
case GroupType.Multi: case GroupType.Multi:
DrawMultiGroup(group, idx); DrawMultiGroup(group, idx);
break; break;
case GroupType.Single when group.Count <= _config.SingleGroupRadioMax: case GroupType.Single when group.Options.Count <= _config.SingleGroupRadioMax:
DrawSingleGroupRadio(group, idx); DrawSingleGroupRadio(group, idx);
break; break;
} }
@ -181,13 +181,14 @@ public class ModPanelSettingsTab : ITab
using var id = ImRaii.PushId(groupIdx); using var id = ImRaii.PushId(groupIdx);
var selectedOption = _empty ? group.DefaultSettings.AsIndex : _settings.Settings[groupIdx].AsIndex; var selectedOption = _empty ? group.DefaultSettings.AsIndex : _settings.Settings[groupIdx].AsIndex;
ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X * 3 / 4); ImGui.SetNextItemWidth(UiHelpers.InputTextWidth.X * 3 / 4);
using (var combo = ImRaii.Combo(string.Empty, group[selectedOption].Name)) var options = group.Options;
using (var combo = ImRaii.Combo(string.Empty, options[selectedOption].Name))
{ {
if (combo) if (combo)
for (var idx2 = 0; idx2 < group.Count; ++idx2) for (var idx2 = 0; idx2 < options.Count; ++idx2)
{ {
id.Push(idx2); id.Push(idx2);
var option = group[idx2]; var option = options[idx2];
if (ImGui.Selectable(option.Name, idx2 == selectedOption)) if (ImGui.Selectable(option.Name, idx2 == selectedOption))
_collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx,
Setting.Single(idx2)); Setting.Single(idx2));
@ -213,18 +214,18 @@ public class ModPanelSettingsTab : ITab
using var id = ImRaii.PushId(groupIdx); using var id = ImRaii.PushId(groupIdx);
var selectedOption = _empty ? group.DefaultSettings.AsIndex : _settings.Settings[groupIdx].AsIndex; var selectedOption = _empty ? group.DefaultSettings.AsIndex : _settings.Settings[groupIdx].AsIndex;
var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); var minWidth = Widget.BeginFramedGroup(group.Name, group.Description);
var options = group.Options;
DrawCollapseHandling(group, minWidth, DrawOptions); DrawCollapseHandling(options, minWidth, DrawOptions);
Widget.EndFramedGroup(); Widget.EndFramedGroup();
return; return;
void DrawOptions() void DrawOptions()
{ {
for (var idx = 0; idx < group.Count; ++idx) for (var idx = 0; idx < group.Options.Count; ++idx)
{ {
using var i = ImRaii.PushId(idx); using var i = ImRaii.PushId(idx);
var option = group[idx]; var option = options[idx];
if (ImGui.RadioButton(option.Name, selectedOption == idx)) if (ImGui.RadioButton(option.Name, selectedOption == idx))
_collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx,
Setting.Single(idx)); Setting.Single(idx));
@ -239,9 +240,9 @@ public class ModPanelSettingsTab : ITab
} }
private void DrawCollapseHandling(IModGroup group, float minWidth, Action draw) private void DrawCollapseHandling(IReadOnlyList<IModOption> options, float minWidth, Action draw)
{ {
if (group.Count <= _config.OptionGroupCollapsibleMin) if (options.Count <= _config.OptionGroupCollapsibleMin)
{ {
draw(); draw();
} }
@ -249,8 +250,8 @@ public class ModPanelSettingsTab : ITab
{ {
var collapseId = ImGui.GetID("Collapse"); var collapseId = ImGui.GetID("Collapse");
var shown = ImGui.GetStateStorage().GetBool(collapseId, true); var shown = ImGui.GetStateStorage().GetBool(collapseId, true);
var buttonTextShow = $"Show {group.Count} Options"; var buttonTextShow = $"Show {options.Count} Options";
var buttonTextHide = $"Hide {group.Count} Options"; var buttonTextHide = $"Hide {options.Count} Options";
var buttonWidth = Math.Max(ImGui.CalcTextSize(buttonTextShow).X, ImGui.CalcTextSize(buttonTextHide).X) var buttonWidth = Math.Max(ImGui.CalcTextSize(buttonTextShow).X, ImGui.CalcTextSize(buttonTextHide).X)
+ 2 * ImGui.GetStyle().FramePadding.X; + 2 * ImGui.GetStyle().FramePadding.X;
minWidth = Math.Max(buttonWidth, minWidth); minWidth = Math.Max(buttonWidth, minWidth);
@ -274,7 +275,7 @@ public class ModPanelSettingsTab : ITab
} }
else else
{ {
var optionWidth = group.Max(o => ImGui.CalcTextSize(o.Name).X) var optionWidth = options.Max(o => ImGui.CalcTextSize(o.Name).X)
+ ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetStyle().ItemInnerSpacing.X
+ ImGui.GetFrameHeight() + ImGui.GetFrameHeight()
+ ImGui.GetStyle().FramePadding.X; + ImGui.GetStyle().FramePadding.X;
@ -294,8 +295,8 @@ public class ModPanelSettingsTab : ITab
using var id = ImRaii.PushId(groupIdx); using var id = ImRaii.PushId(groupIdx);
var flags = _empty ? group.DefaultSettings : _settings.Settings[groupIdx]; var flags = _empty ? group.DefaultSettings : _settings.Settings[groupIdx];
var minWidth = Widget.BeginFramedGroup(group.Name, group.Description); var minWidth = Widget.BeginFramedGroup(group.Name, group.Description);
var options = group.Options;
DrawCollapseHandling(group, minWidth, DrawOptions); DrawCollapseHandling(options, minWidth, DrawOptions);
Widget.EndFramedGroup(); Widget.EndFramedGroup();
var label = $"##multi{groupIdx}"; var label = $"##multi{groupIdx}";
@ -307,10 +308,10 @@ public class ModPanelSettingsTab : ITab
void DrawOptions() void DrawOptions()
{ {
for (var idx = 0; idx < group.Count; ++idx) for (var idx = 0; idx < options.Count; ++idx)
{ {
using var i = ImRaii.PushId(idx); using var i = ImRaii.PushId(idx);
var option = group[idx]; var option = options[idx];
var setting = flags.HasFlag(idx); var setting = flags.HasFlag(idx);
if (ImGui.Checkbox(option.Name, ref setting)) if (ImGui.Checkbox(option.Name, ref setting))
@ -339,7 +340,7 @@ public class ModPanelSettingsTab : ITab
ImGui.Separator(); ImGui.Separator();
if (ImGui.Selectable("Enable All")) if (ImGui.Selectable("Enable All"))
_collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx,
Setting.AllBits(group.Count)); Setting.AllBits(group.Options.Count));
if (ImGui.Selectable("Disable All")) if (ImGui.Selectable("Disable All"))
_collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, Setting.Zero); _collectionManager.Editor.SetModSetting(_collectionManager.Active.Current, _selector.Selected!, groupIdx, Setting.Zero);