This sucks so hard...

This commit is contained in:
Ottermandias 2024-04-24 23:04:04 +02:00
parent 07afbfb229
commit 6b1743b776
33 changed files with 852 additions and 695 deletions

View file

@ -201,7 +201,7 @@ public class ModSettingsApi : IPenumbraApiModSettings, IApiService, IDisposable
{ {
foreach (var name in optionNames) foreach (var name in optionNames)
{ {
var optionIdx = multi.PrioritizedOptions.IndexOf(o => o.Mod.Name == name); var optionIdx = multi.OptionData.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

@ -152,7 +152,7 @@ public partial class TexToolsImporter
} }
// Iterate through all pages // Iterate through all pages
var options = new List<SubMod>(); var options = new List<MultiSubMod>();
var groupPriority = ModPriority.Default; var groupPriority = ModPriority.Default;
var groupNames = new HashSet<string>(); var groupNames = new HashSet<string>();
foreach (var page in modList.ModPackPages) foreach (var page in modList.ModPackPages)
@ -183,7 +183,7 @@ public partial class TexToolsImporter
var optionFolder = ModCreator.NewSubFolderName(groupFolder, option.Name, _config.ReplaceNonAsciiOnImport) var optionFolder = ModCreator.NewSubFolderName(groupFolder, option.Name, _config.ReplaceNonAsciiOnImport)
?? new DirectoryInfo(Path.Combine(groupFolder.FullName, $"Option {i + optionIdx + 1}")); ?? new DirectoryInfo(Path.Combine(groupFolder.FullName, $"Option {i + optionIdx + 1}"));
ExtractSimpleModList(optionFolder, option.ModsJsons); ExtractSimpleModList(optionFolder, option.ModsJsons);
options.Add(_modManager.Creator.CreateSubMod(_currentModDirectory, optionFolder, option)); options.Add(_modManager.Creator.CreateSubMod(_currentModDirectory, optionFolder, option, new ModPriority(i)));
if (option.IsChecked) if (option.IsChecked)
defaultSettings = group.SelectionType == GroupType.Multi defaultSettings = group.SelectionType == GroupType.Multi
? defaultSettings!.Value | Setting.Multi(i) ? defaultSettings!.Value | Setting.Multi(i)
@ -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, SubMod.CreateForSaving(option.Name)); options.Insert(idx, MultiSubMod.CreateForSaving(option.Name, option.Description, ModPriority.Default));
if (option.IsChecked) if (option.IsChecked)
defaultSettings = Setting.Single(idx); defaultSettings = Setting.Single(idx);
} }

View file

@ -50,17 +50,15 @@ public unsafe class MetaFileManager
TexToolsMeta.WriteTexToolsMeta(this, mod.Default.Manipulations, mod.ModPath); TexToolsMeta.WriteTexToolsMeta(this, mod.Default.Manipulations, mod.ModPath);
foreach (var group in mod.Groups) foreach (var group in mod.Groups)
{ {
if (group is not ITexToolsGroup texToolsGroup)
continue;
var dir = ModCreator.NewOptionDirectory(mod.ModPath, group.Name, Config.ReplaceNonAsciiOnImport); var dir = ModCreator.NewOptionDirectory(mod.ModPath, group.Name, Config.ReplaceNonAsciiOnImport);
if (!dir.Exists) if (!dir.Exists)
dir.Create(); dir.Create();
var optionEnumerator = group switch
{ foreach (var option in texToolsGroup.OptionData)
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)
@ -99,7 +97,7 @@ public unsafe class MetaFileManager
return; return;
ResidentResources.Reload(); ResidentResources.Reload();
if (collection?._cache == null) if (collection._cache == null)
CharacterUtility.ResetAll(); CharacterUtility.ResetAll();
else else
collection._cache.Meta.SetFiles(); collection._cache.Meta.SetFiles();

View file

@ -29,7 +29,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co
Worker = Task.Run(() => CheckDuplicates(filesTmp, _cancellationTokenSource.Token), _cancellationTokenSource.Token); Worker = Task.Run(() => CheckDuplicates(filesTmp, _cancellationTokenSource.Token), _cancellationTokenSource.Token);
} }
public void DeleteDuplicates(ModFileCollection files, Mod mod, SubMod option, bool useModManager) public void DeleteDuplicates(ModFileCollection files, Mod mod, IModDataContainer option, bool useModManager)
{ {
if (!Worker.IsCompleted || _duplicates.Count == 0) if (!Worker.IsCompleted || _duplicates.Count == 0)
return; return;
@ -72,7 +72,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co
return; return;
void HandleSubMod(SubMod subMod, int groupIdx, int optionIdx) void HandleSubMod(IModDataContainer subMod, int groupIdx, int optionIdx)
{ {
var changes = false; var changes = false;
var dict = subMod.Files.ToDictionary(kvp => kvp.Key, var dict = subMod.Files.ToDictionary(kvp => kvp.Key,
@ -86,7 +86,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co
} }
else else
{ {
subMod.FileData = dict; subMod.Files = dict;
saveService.ImmediateSaveSync(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); saveService.ImmediateSaveSync(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
} }
} }

View file

@ -5,12 +5,12 @@ namespace Penumbra.Mods.Editor;
public class FileRegistry : IEquatable<FileRegistry> public class FileRegistry : IEquatable<FileRegistry>
{ {
public readonly List<(SubMod, Utf8GamePath)> SubModUsage = []; public readonly List<(IModDataContainer, Utf8GamePath)> SubModUsage = [];
public FullPath File { get; private init; } public FullPath File { get; private init; }
public Utf8RelPath RelPath { get; private init; } public Utf8RelPath RelPath { get; private init; }
public long FileSize { get; private init; } public long FileSize { get; private init; }
public int CurrentUsage; public int CurrentUsage;
public bool IsOnPlayer; public bool IsOnPlayer;
public static bool FromFile(DirectoryInfo modPath, FileInfo file, [NotNullWhen(true)] out FileRegistry? registry) public static bool FromFile(DirectoryInfo modPath, FileInfo file, [NotNullWhen(true)] out FileRegistry? registry)
{ {

View file

@ -1,4 +1,3 @@
using System;
using OtterGui; using OtterGui;
using OtterGui.Compression; using OtterGui.Compression;
using Penumbra.Mods.Subclasses; using Penumbra.Mods.Subclasses;
@ -25,20 +24,20 @@ public class ModEditor(
public readonly MdlMaterialEditor MdlMaterialEditor = mdlMaterialEditor; public readonly MdlMaterialEditor MdlMaterialEditor = mdlMaterialEditor;
public readonly FileCompactor Compactor = compactor; public readonly FileCompactor Compactor = compactor;
public Mod? Mod { get; private set; } public Mod? Mod { get; private set; }
public int GroupIdx { get; private set; } public int GroupIdx { get; private set; }
public int OptionIdx { get; private set; } public int DataIdx { get; private set; }
public IModGroup? Group { get; private set; } public IModGroup? Group { get; private set; }
public SubMod? Option { get; private set; } public IModDataContainer? Option { get; private set; }
public void LoadMod(Mod mod) public void LoadMod(Mod mod)
=> LoadMod(mod, -1, 0); => LoadMod(mod, -1, 0);
public void LoadMod(Mod mod, int groupIdx, int optionIdx) public void LoadMod(Mod mod, int groupIdx, int dataIdx)
{ {
Mod = mod; Mod = mod;
LoadOption(groupIdx, optionIdx, true); LoadOption(groupIdx, dataIdx, true);
Files.UpdateAll(mod, Option!); Files.UpdateAll(mod, Option!);
SwapEditor.Revert(Option!); SwapEditor.Revert(Option!);
MetaEditor.Load(Mod!, Option!); MetaEditor.Load(Mod!, Option!);
@ -46,9 +45,9 @@ public class ModEditor(
MdlMaterialEditor.ScanModels(Mod!); MdlMaterialEditor.ScanModels(Mod!);
} }
public void LoadOption(int groupIdx, int optionIdx) public void LoadOption(int groupIdx, int dataIdx)
{ {
LoadOption(groupIdx, optionIdx, true); LoadOption(groupIdx, dataIdx, true);
SwapEditor.Revert(Option!); SwapEditor.Revert(Option!);
Files.UpdatePaths(Mod!, Option!); Files.UpdatePaths(Mod!, Option!);
MetaEditor.Load(Mod!, Option!); MetaEditor.Load(Mod!, Option!);
@ -57,44 +56,38 @@ public class ModEditor(
} }
/// <summary> Load the correct option by indices for the currently loaded mod if possible, unload if not. </summary> /// <summary> Load the correct option by indices for the currently loaded mod if possible, unload if not. </summary>
private void LoadOption(int groupIdx, int optionIdx, bool message) private void LoadOption(int groupIdx, int dataIdx, bool message)
{ {
if (Mod != null && Mod.Groups.Count > groupIdx) if (Mod != null && Mod.Groups.Count > groupIdx)
{ {
if (groupIdx == -1 && optionIdx == 0) if (groupIdx == -1 && dataIdx == 0)
{ {
Group = null; Group = null;
Option = Mod.Default; Option = Mod.Default;
GroupIdx = groupIdx; GroupIdx = groupIdx;
OptionIdx = optionIdx; DataIdx = dataIdx;
return; return;
} }
if (groupIdx >= 0) if (groupIdx >= 0)
{ {
Group = Mod.Groups[groupIdx]; Group = Mod.Groups[groupIdx];
switch(Group) if (dataIdx >= 0 && dataIdx < Group.DataContainers.Count)
{ {
case SingleModGroup single when optionIdx >= 0 && optionIdx < single.OptionData.Count: Option = Group.DataContainers[dataIdx];
Option = single.OptionData[optionIdx]; GroupIdx = groupIdx;
GroupIdx = groupIdx; DataIdx = dataIdx;
OptionIdx = optionIdx; return;
return;
case MultiModGroup multi when optionIdx >= 0 && optionIdx < multi.PrioritizedOptions.Count:
Option = multi.PrioritizedOptions[optionIdx].Mod;
GroupIdx = groupIdx;
OptionIdx = optionIdx;
return;
} }
} }
} }
Group = null; Group = null;
Option = Mod?.Default; Option = Mod?.Default;
GroupIdx = -1; GroupIdx = -1;
OptionIdx = 0; DataIdx = 0;
if (message) if (message)
Penumbra.Log.Error($"Loading invalid option {groupIdx} {optionIdx} for Mod {Mod?.Name ?? "Unknown"}."); Penumbra.Log.Error($"Loading invalid option {groupIdx} {dataIdx} for Mod {Mod?.Name ?? "Unknown"}.");
} }
public void Clear() public void Clear()
@ -111,7 +104,7 @@ public class ModEditor(
=> Clear(); => Clear();
/// <summary> Apply a option action to all available option in a mod, including the default option. </summary> /// <summary> Apply a option action to all available option in a mod, including the default option. </summary>
public static void ApplyToAllOptions(Mod mod, Action<SubMod, int, int> action) public static void ApplyToAllOptions(Mod mod, Action<IModDataContainer, int, int> action)
{ {
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())
@ -123,8 +116,8 @@ public class ModEditor(
action(single.OptionData[optionIdx], groupIdx, optionIdx); action(single.OptionData[optionIdx], groupIdx, optionIdx);
break; break;
case MultiModGroup multi: case MultiModGroup multi:
for (var optionIdx = 0; optionIdx < multi.PrioritizedOptions.Count; ++optionIdx) for (var optionIdx = 0; optionIdx < multi.OptionData.Count; ++optionIdx)
action(multi.PrioritizedOptions[optionIdx].Mod, groupIdx, optionIdx); action(multi.OptionData[optionIdx], groupIdx, optionIdx);
break; break;
} }
} }

View file

@ -38,13 +38,13 @@ public class ModFileCollection : IDisposable
public bool Ready { get; private set; } = true; public bool Ready { get; private set; } = true;
public void UpdateAll(Mod mod, SubMod option) public void UpdateAll(Mod mod, IModDataContainer option)
{ {
UpdateFiles(mod, new CancellationToken()); UpdateFiles(mod, new CancellationToken());
UpdatePaths(mod, option, false, new CancellationToken()); UpdatePaths(mod, option, false, new CancellationToken());
} }
public void UpdatePaths(Mod mod, SubMod option) public void UpdatePaths(Mod mod, IModDataContainer option)
=> UpdatePaths(mod, option, true, new CancellationToken()); => UpdatePaths(mod, option, true, new CancellationToken());
public void Clear() public void Clear()
@ -59,7 +59,7 @@ public class ModFileCollection : IDisposable
public void ClearMissingFiles() public void ClearMissingFiles()
=> _missing.Clear(); => _missing.Clear();
public void RemoveUsedPath(SubMod option, FileRegistry? file, Utf8GamePath gamePath) public void RemoveUsedPath(IModDataContainer option, FileRegistry? file, Utf8GamePath gamePath)
{ {
_usedPaths.Remove(gamePath); _usedPaths.Remove(gamePath);
if (file != null) if (file != null)
@ -69,10 +69,10 @@ public class ModFileCollection : IDisposable
} }
} }
public void RemoveUsedPath(SubMod option, FullPath file, Utf8GamePath gamePath) public void RemoveUsedPath(IModDataContainer option, FullPath file, Utf8GamePath gamePath)
=> RemoveUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath); => RemoveUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath);
public void AddUsedPath(SubMod option, FileRegistry? file, Utf8GamePath gamePath) public void AddUsedPath(IModDataContainer option, FileRegistry? file, Utf8GamePath gamePath)
{ {
_usedPaths.Add(gamePath); _usedPaths.Add(gamePath);
if (file == null) if (file == null)
@ -82,7 +82,7 @@ public class ModFileCollection : IDisposable
file.SubModUsage.Add((option, gamePath)); file.SubModUsage.Add((option, gamePath));
} }
public void AddUsedPath(SubMod option, FullPath file, Utf8GamePath gamePath) public void AddUsedPath(IModDataContainer option, FullPath file, Utf8GamePath gamePath)
=> AddUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath); => AddUsedPath(option, _available.FirstOrDefault(f => f.File.Equals(file)), gamePath);
public void ChangeUsedPath(FileRegistry file, int pathIdx, Utf8GamePath gamePath) public void ChangeUsedPath(FileRegistry file, int pathIdx, Utf8GamePath gamePath)
@ -154,14 +154,14 @@ public class ModFileCollection : IDisposable
_usedPaths.Clear(); _usedPaths.Clear();
} }
private void UpdatePaths(Mod mod, SubMod option, bool clearRegistries, CancellationToken tok) private void UpdatePaths(Mod mod, IModDataContainer option, bool clearRegistries, CancellationToken tok)
{ {
tok.ThrowIfCancellationRequested(); tok.ThrowIfCancellationRequested();
ClearPaths(clearRegistries, tok); ClearPaths(clearRegistries, tok);
tok.ThrowIfCancellationRequested(); tok.ThrowIfCancellationRequested();
foreach (var subMod in mod.AllSubMods) foreach (var subMod in mod.AllDataContainers)
{ {
foreach (var (gamePath, file) in subMod.Files) foreach (var (gamePath, file) in subMod.Files)
{ {

View file

@ -14,7 +14,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu
Changes = false; Changes = false;
} }
public int Apply(Mod mod, SubMod option) public int Apply(Mod mod, IModDataContainer option)
{ {
var dict = new Dictionary<Utf8GamePath, FullPath>(); var dict = new Dictionary<Utf8GamePath, FullPath>();
var num = 0; var num = 0;
@ -24,23 +24,23 @@ 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;
} }
var (groupIdx, optionIdx) = option.GetIndices(); var (groupIdx, dataIdx) = option.GetDataIndices();
modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, dict); modManager.OptionEditor.OptionSetFiles(mod, groupIdx, dataIdx, dict);
files.UpdatePaths(mod, option); files.UpdatePaths(mod, option);
Changes = false; Changes = false;
return num; return num;
} }
public void Revert(Mod mod, SubMod option) public void Revert(Mod mod, IModDataContainer option)
{ {
files.UpdateAll(mod, option); files.UpdateAll(mod, option);
Changes = false; Changes = false;
} }
/// <summary> Remove all path redirections where the pointed-to file does not exist. </summary> /// <summary> Remove all path redirections where the pointed-to file does not exist. </summary>
public void RemoveMissingPaths(Mod mod, SubMod option) public void RemoveMissingPaths(Mod mod, IModDataContainer option)
{ {
void HandleSubMod(SubMod subMod, int groupIdx, int optionIdx) void HandleSubMod(IModDataContainer subMod, int groupIdx, int optionIdx)
{ {
var newDict = subMod.Files.Where(kvp => CheckAgainstMissing(mod, subMod, kvp.Value, kvp.Key, subMod == option)) var newDict = subMod.Files.Where(kvp => CheckAgainstMissing(mod, subMod, kvp.Value, kvp.Key, subMod == option))
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); .ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
@ -62,7 +62,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu
/// If path is empty, it will be deleted instead. /// If path is empty, it will be deleted instead.
/// If pathIdx is equal to the total number of paths, path will be added, otherwise replaced. /// If pathIdx is equal to the total number of paths, path will be added, otherwise replaced.
/// </summary> /// </summary>
public bool SetGamePath(SubMod option, int fileIdx, int pathIdx, Utf8GamePath path) public bool SetGamePath(IModDataContainer option, int fileIdx, int pathIdx, Utf8GamePath path)
{ {
if (!CanAddGamePath(path) || fileIdx < 0 || fileIdx > files.Available.Count) if (!CanAddGamePath(path) || fileIdx < 0 || fileIdx > files.Available.Count)
return false; return false;
@ -85,7 +85,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu
/// Transform a set of files to the appropriate game paths with the given number of folders skipped, /// Transform a set of files to the appropriate game paths with the given number of folders skipped,
/// and add them to the given option. /// and add them to the given option.
/// </summary> /// </summary>
public int AddPathsToSelected(SubMod option, IEnumerable<FileRegistry> files1, int skipFolders = 0) public int AddPathsToSelected(IModDataContainer option, IEnumerable<FileRegistry> files1, int skipFolders = 0)
{ {
var failed = 0; var failed = 0;
foreach (var file in files1) foreach (var file in files1)
@ -112,7 +112,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu
} }
/// <summary> Remove all paths in the current option from the given files. </summary> /// <summary> Remove all paths in the current option from the given files. </summary>
public void RemovePathsFromSelected(SubMod option, IEnumerable<FileRegistry> files1) public void RemovePathsFromSelected(IModDataContainer option, IEnumerable<FileRegistry> files1)
{ {
foreach (var file in files1) foreach (var file in files1)
{ {
@ -130,7 +130,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu
} }
/// <summary> Delete all given files from your filesystem </summary> /// <summary> Delete all given files from your filesystem </summary>
public void DeleteFiles(Mod mod, SubMod option, IEnumerable<FileRegistry> files1) public void DeleteFiles(Mod mod, IModDataContainer option, IEnumerable<FileRegistry> files1)
{ {
var deletions = 0; var deletions = 0;
foreach (var file in files1) foreach (var file in files1)
@ -156,7 +156,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu
} }
private bool CheckAgainstMissing(Mod mod, SubMod option, FullPath file, Utf8GamePath key, bool removeUsed) private bool CheckAgainstMissing(Mod mod, IModDataContainer option, FullPath file, Utf8GamePath key, bool removeUsed)
{ {
if (!files.Missing.Contains(file)) if (!files.Missing.Contains(file))
return true; return true;

View file

@ -1,6 +1,5 @@
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;
@ -33,9 +32,9 @@ public class ModMerger : IDisposable
private readonly Dictionary<string, string> _fileToFile = []; private readonly Dictionary<string, string> _fileToFile = [];
private readonly HashSet<string> _createdDirectories = []; private readonly HashSet<string> _createdDirectories = [];
private readonly HashSet<int> _createdGroups = []; private readonly HashSet<int> _createdGroups = [];
private readonly HashSet<SubMod> _createdOptions = []; private readonly HashSet<IModDataOption> _createdOptions = [];
public readonly HashSet<SubMod> SelectedOptions = []; public readonly HashSet<IModDataContainer> SelectedOptions = [];
public readonly IReadOnlyList<string> Warnings = []; public readonly IReadOnlyList<string> Warnings = [];
public Exception? Error { get; private set; } public Exception? Error { get; private set; }
@ -94,7 +93,7 @@ public class ModMerger : IDisposable
private void MergeWithOptions() private void MergeWithOptions()
{ {
MergeIntoOption(Enumerable.Repeat(MergeFromMod!.Default, 1), MergeToMod!.Default, false); MergeIntoOption([MergeFromMod!.Default], MergeToMod!.Default, false);
foreach (var originalGroup in MergeFromMod!.Groups) foreach (var originalGroup in MergeFromMod!.Groups)
{ {
@ -105,20 +104,13 @@ 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}.");
var optionEnumerator = group switch foreach (var originalOption in group.DataContainers)
{ {
SingleModGroup single => single.OptionData, var (option, _, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, originalOption.GetName());
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((IModDataOption)option);
MergeIntoOption(Enumerable.Repeat(originalOption, 1), option, false); MergeIntoOption([originalOption], (IModDataOption)option, false);
} }
else else
{ {
@ -136,7 +128,7 @@ public class ModMerger : IDisposable
if (groupName.Length == 0 && optionName.Length == 0) if (groupName.Length == 0 && optionName.Length == 0)
{ {
CopyFiles(MergeToMod!.ModPath); CopyFiles(MergeToMod!.ModPath);
MergeIntoOption(MergeFromMod!.AllSubMods.Reverse(), MergeToMod!.Default, true); MergeIntoOption(MergeFromMod!.AllDataContainers.Reverse(), MergeToMod!.Default, true);
} }
else if (groupName.Length * optionName.Length == 0) else if (groupName.Length * optionName.Length == 0)
{ {
@ -148,7 +140,7 @@ public class ModMerger : IDisposable
_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((IModDataOption)option);
var dir = ModCreator.NewOptionDirectory(MergeToMod!.ModPath, groupName, _config.ReplaceNonAsciiOnImport); var dir = ModCreator.NewOptionDirectory(MergeToMod!.ModPath, groupName, _config.ReplaceNonAsciiOnImport);
if (!dir.Exists) if (!dir.Exists)
_createdDirectories.Add(dir.FullName); _createdDirectories.Add(dir.FullName);
@ -156,14 +148,14 @@ public class ModMerger : IDisposable
if (!dir.Exists) if (!dir.Exists)
_createdDirectories.Add(dir.FullName); _createdDirectories.Add(dir.FullName);
CopyFiles(dir); CopyFiles(dir);
MergeIntoOption(MergeFromMod!.AllSubMods.Reverse(), option, true); MergeIntoOption(MergeFromMod!.AllDataContainers.Reverse(), (IModDataOption)option, true);
} }
private void MergeIntoOption(IEnumerable<SubMod> mergeOptions, SubMod option, bool fromFileToFile) private void MergeIntoOption(IEnumerable<IModDataContainer> mergeOptions, IModDataContainer option, bool fromFileToFile)
{ {
var redirections = option.FileData.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); var redirections = option.Files.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
var swaps = option.FileSwapData.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); var swaps = option.FileSwaps.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
var manips = option.ManipulationData.ToHashSet(); var manips = option.Manipulations.ToHashSet();
foreach (var originalOption in mergeOptions) foreach (var originalOption in mergeOptions)
{ {
@ -171,31 +163,31 @@ public class ModMerger : IDisposable
{ {
if (!manips.Add(manip)) if (!manips.Add(manip))
throw new Exception( throw new Exception(
$"Could not add meta manipulation {manip} from {originalOption.FullName} to {option.FullName} because another manipulation of the same data already exists in this option."); $"Could not add meta manipulation {manip} from {originalOption.GetFullName()} to {option.GetFullName()} because another manipulation of the same data already exists in this option.");
} }
foreach (var (swapA, swapB) in originalOption.FileSwaps) foreach (var (swapA, swapB) in originalOption.FileSwaps)
{ {
if (!swaps.TryAdd(swapA, swapB)) if (!swaps.TryAdd(swapA, swapB))
throw new Exception( throw new Exception(
$"Could not add file swap {swapB} -> {swapA} from {originalOption.FullName} to {option.FullName} because another swap of the key already exists."); $"Could not add file swap {swapB} -> {swapA} from {originalOption.GetFullName()} to {option.GetFullName()} because another swap of the key already exists.");
} }
foreach (var (gamePath, path) in originalOption.Files) foreach (var (gamePath, path) in originalOption.Files)
{ {
if (!GetFullPath(path, out var newFile)) if (!GetFullPath(path, out var newFile))
throw new Exception( throw new Exception(
$"Could not add file redirection {path} -> {gamePath} from {originalOption.FullName} to {option.FullName} because the file does not exist in the new mod."); $"Could not add file redirection {path} -> {gamePath} from {originalOption.GetFullName()} to {option.GetFullName()} because the file does not exist in the new mod.");
if (!redirections.TryAdd(gamePath, newFile)) if (!redirections.TryAdd(gamePath, newFile))
throw new Exception( throw new Exception(
$"Could not add file redirection {path} -> {gamePath} from {originalOption.FullName} to {option.FullName} because a redirection for the game path already exists."); $"Could not add file redirection {path} -> {gamePath} from {originalOption.GetFullName()} to {option.GetFullName()} because a redirection for the game path already exists.");
} }
} }
var (groupIdx, optionIdx) = option.GetIndices(); var (groupIdx, dataIdx) = option.GetDataIndices();
_editor.OptionSetFiles(MergeToMod!, groupIdx, optionIdx, redirections, SaveType.None); _editor.OptionSetFiles(MergeToMod!, groupIdx, dataIdx, redirections, SaveType.None);
_editor.OptionSetFileSwaps(MergeToMod!, groupIdx, optionIdx, swaps, SaveType.None); _editor.OptionSetFileSwaps(MergeToMod!, groupIdx, dataIdx, swaps, SaveType.None);
_editor.OptionSetManipulations(MergeToMod!, groupIdx, optionIdx, manips, SaveType.ImmediateSync); _editor.OptionSetManipulations(MergeToMod!, groupIdx, dataIdx, manips, SaveType.ImmediateSync);
return; return;
bool GetFullPath(FullPath input, out FullPath ret) bool GetFullPath(FullPath input, out FullPath ret)
@ -270,30 +262,29 @@ public class ModMerger : IDisposable
{ {
var files = CopySubModFiles(mods[0], dir); var files = CopySubModFiles(mods[0], dir);
_editor.OptionSetFiles(result, -1, 0, files); _editor.OptionSetFiles(result, -1, 0, files);
_editor.OptionSetFileSwaps(result, -1, 0, mods[0].FileSwapData); _editor.OptionSetFileSwaps(result, -1, 0, mods[0].FileSwaps);
_editor.OptionSetManipulations(result, -1, 0, mods[0].ManipulationData); _editor.OptionSetManipulations(result, -1, 0, mods[0].Manipulations);
} }
else else
{ {
foreach (var originalOption in mods) foreach (var originalOption in mods)
{ {
if (originalOption.IsDefault) if (originalOption.Group is not {} originalGroup)
{ {
var files = CopySubModFiles(mods[0], dir); var files = CopySubModFiles(mods[0], dir);
_editor.OptionSetFiles(result, -1, 0, files); _editor.OptionSetFiles(result, -1, 0, files);
_editor.OptionSetFileSwaps(result, -1, 0, mods[0].FileSwapData); _editor.OptionSetFileSwaps(result, -1, 0, mods[0].FileSwaps);
_editor.OptionSetManipulations(result, -1, 0, mods[0].ManipulationData); _editor.OptionSetManipulations(result, -1, 0, mods[0].Manipulations);
} }
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, optionIdx, _) = _editor.FindOrAddOption(result, groupIdx, originalOption.GetName());
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, optionIdx, files); _editor.OptionSetFiles(result, groupIdx, optionIdx, files);
_editor.OptionSetFileSwaps(result, groupIdx, optionIdx, originalOption.FileSwapData); _editor.OptionSetFileSwaps(result, groupIdx, optionIdx, originalOption.FileSwaps);
_editor.OptionSetManipulations(result, groupIdx, optionIdx, originalOption.ManipulationData); _editor.OptionSetManipulations(result, groupIdx, optionIdx, originalOption.Manipulations);
} }
} }
} }
@ -315,11 +306,11 @@ public class ModMerger : IDisposable
} }
} }
private static Dictionary<Utf8GamePath, FullPath> CopySubModFiles(SubMod option, DirectoryInfo newMod) private static Dictionary<Utf8GamePath, FullPath> CopySubModFiles(IModDataContainer option, DirectoryInfo newMod)
{ {
var ret = new Dictionary<Utf8GamePath, FullPath>(option.FileData.Count); var ret = new Dictionary<Utf8GamePath, FullPath>(option.Files.Count);
var parentPath = ((Mod)option.Mod).ModPath.FullName; var parentPath = ((Mod)option.Mod).ModPath.FullName;
foreach (var (path, file) in option.FileData) foreach (var (path, file) in option.Files)
{ {
var target = Path.GetRelativePath(parentPath, file.FullName); var target = Path.GetRelativePath(parentPath, file.FullName);
target = Path.Combine(newMod.FullName, target); target = Path.Combine(newMod.FullName, target);
@ -348,7 +339,7 @@ public class ModMerger : IDisposable
{ {
foreach (var option in _createdOptions) foreach (var option in _createdOptions)
{ {
var (groupIdx, optionIdx) = option.GetIndices(); var (groupIdx, optionIdx) = option.GetOptionIndices();
_editor.DeleteOption(MergeToMod!, groupIdx, optionIdx); _editor.DeleteOption(MergeToMod!, groupIdx, optionIdx);
Penumbra.Log.Verbose($"[Merger] Removed option {option.FullName}."); Penumbra.Log.Verbose($"[Merger] Removed option {option.FullName}.");
} }

View file

@ -103,7 +103,7 @@ public class ModMetaEditor(ModManager modManager)
Changes = true; Changes = true;
} }
public void Load(Mod mod, SubMod currentOption) public void Load(Mod mod, IModDataContainer currentOption)
{ {
OtherImcCount = 0; OtherImcCount = 0;
OtherEqpCount = 0; OtherEqpCount = 0;
@ -111,7 +111,7 @@ public class ModMetaEditor(ModManager modManager)
OtherGmpCount = 0; OtherGmpCount = 0;
OtherEstCount = 0; OtherEstCount = 0;
OtherRspCount = 0; OtherRspCount = 0;
foreach (var option in mod.AllSubMods) foreach (var option in mod.AllDataContainers)
{ {
if (option == currentOption) if (option == currentOption)
continue; continue;

View file

@ -1,3 +1,4 @@
using System;
using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.Notifications;
using OtterGui; using OtterGui;
using OtterGui.Classes; using OtterGui.Classes;
@ -168,27 +169,11 @@ public class ModNormalizer(ModManager _modManager, Configuration _config)
foreach (var (group, groupIdx) in Mod.Groups.WithIndex()) foreach (var (group, groupIdx) in Mod.Groups.WithIndex())
{ {
var groupDir = ModCreator.CreateModFolder(directory, group.Name, _config.ReplaceNonAsciiOnImport, true); var groupDir = ModCreator.CreateModFolder(directory, group.Name, _config.ReplaceNonAsciiOnImport, true);
switch (group) _redirections[groupIdx + 1].EnsureCapacity(group.DataContainers.Count);
{ for (var i = _redirections[groupIdx + 1].Count; i < group.DataContainers.Count; ++i)
case SingleModGroup single: _redirections[groupIdx + 1].Add([]);
_redirections[groupIdx + 1].EnsureCapacity(single.OptionData.Count); foreach (var (data, dataIdx) in group.DataContainers.WithIndex())
for (var i = _redirections[groupIdx + 1].Count; i < single.OptionData.Count; ++i) HandleSubMod(groupDir, data, _redirections[groupIdx + 1][dataIdx]);
_redirections[groupIdx + 1].Add([]);
foreach (var (option, optionIdx) in single.OptionData.WithIndex())
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; return true;
@ -200,13 +185,14 @@ public class ModNormalizer(ModManager _modManager, Configuration _config)
return false; return false;
void HandleSubMod(DirectoryInfo groupDir, SubMod option, Dictionary<Utf8GamePath, FullPath> newDict) void HandleSubMod(DirectoryInfo groupDir, IModDataContainer option, Dictionary<Utf8GamePath, FullPath> newDict)
{ {
var optionDir = ModCreator.CreateModFolder(groupDir, option.Name, _config.ReplaceNonAsciiOnImport, true); var name = option.GetName();
var optionDir = ModCreator.CreateModFolder(groupDir, name, _config.ReplaceNonAsciiOnImport, true);
newDict.Clear(); newDict.Clear();
newDict.EnsureCapacity(option.FileData.Count); newDict.EnsureCapacity(option.Files.Count);
foreach (var (gamePath, fullPath) in option.FileData) foreach (var (gamePath, fullPath) in option.Files)
{ {
var relPath = new Utf8RelPath(gamePath).ToString(); var relPath = new Utf8RelPath(gamePath).ToString();
var newFullPath = Path.Combine(optionDir.FullName, relPath); var newFullPath = Path.Combine(optionDir.FullName, relPath);
@ -300,7 +286,7 @@ public class ModNormalizer(ModManager _modManager, Configuration _config)
_modManager.OptionEditor.OptionSetFiles(Mod, groupIdx, optionIdx, _redirections[groupIdx + 1][optionIdx]); _modManager.OptionEditor.OptionSetFiles(Mod, groupIdx, optionIdx, _redirections[groupIdx + 1][optionIdx]);
break; break;
case MultiModGroup multi: case MultiModGroup multi:
foreach (var (_, optionIdx) in multi.PrioritizedOptions.WithIndex()) foreach (var (_, optionIdx) in multi.OptionData.WithIndex())
_modManager.OptionEditor.OptionSetFiles(Mod, groupIdx, optionIdx, _redirections[groupIdx + 1][optionIdx]); _modManager.OptionEditor.OptionSetFiles(Mod, groupIdx, optionIdx, _redirections[groupIdx + 1][optionIdx]);
break; break;
} }

View file

@ -11,7 +11,7 @@ public class ModSwapEditor(ModManager modManager)
public IReadOnlyDictionary<Utf8GamePath, FullPath> Swaps public IReadOnlyDictionary<Utf8GamePath, FullPath> Swaps
=> _swaps; => _swaps;
public void Revert(SubMod option) public void Revert(IModDataContainer option)
{ {
_swaps.SetTo(option.FileSwaps); _swaps.SetTo(option.FileSwaps);
Changes = false; Changes = false;

View file

@ -176,13 +176,13 @@ public class ModCacheManager : IDisposable
} }
private static void UpdateFileCount(Mod mod) private static void UpdateFileCount(Mod mod)
=> mod.TotalFileCount = mod.AllSubMods.Sum(s => s.Files.Count); => mod.TotalFileCount = mod.AllDataContainers.Sum(s => s.Files.Count);
private static void UpdateSwapCount(Mod mod) private static void UpdateSwapCount(Mod mod)
=> mod.TotalSwapCount = mod.AllSubMods.Sum(s => s.FileSwaps.Count); => mod.TotalSwapCount = mod.AllDataContainers.Sum(s => s.FileSwaps.Count);
private static void UpdateMetaCount(Mod mod) private static void UpdateMetaCount(Mod mod)
=> mod.TotalManipulations = mod.AllSubMods.Sum(s => s.Manipulations.Count); => mod.TotalManipulations = mod.AllDataContainers.Sum(s => s.Manipulations.Count);
private static void UpdateHasOptions(Mod mod) private static void UpdateHasOptions(Mod mod)
=> mod.HasOptions = mod.Groups.Any(o => o.IsOption); => mod.HasOptions = mod.Groups.Any(o => o.IsOption);
@ -194,10 +194,10 @@ public class ModCacheManager : IDisposable
{ {
var changedItems = (SortedList<string, object?>)mod.ChangedItems; var changedItems = (SortedList<string, object?>)mod.ChangedItems;
changedItems.Clear(); changedItems.Clear();
foreach (var gamePath in mod.AllSubMods.SelectMany(m => m.Files.Keys.Concat(m.FileSwaps.Keys))) foreach (var gamePath in mod.AllDataContainers.SelectMany(m => m.Files.Keys.Concat(m.FileSwaps.Keys)))
_identifier.Identify(changedItems, gamePath.ToString()); _identifier.Identify(changedItems, gamePath.ToString());
foreach (var manip in mod.AllSubMods.SelectMany(m => m.Manipulations)) foreach (var manip in mod.AllDataContainers.SelectMany(m => m.Manipulations))
ComputeChangedItems(_identifier, changedItems, manip); ComputeChangedItems(_identifier, changedItems, manip);
mod.LowerChangedItemsString = string.Join("\0", mod.ChangedItems.Keys.Select(k => k.ToLowerInvariant())); mod.LowerChangedItemsString = string.Join("\0", mod.ChangedItems.Keys.Select(k => k.ToLowerInvariant()));
@ -211,20 +211,11 @@ public class ModCacheManager : IDisposable
mod.HasOptions = false; mod.HasOptions = false;
foreach (var group in mod.Groups) foreach (var group in mod.Groups)
{ {
mod.HasOptions |= group.IsOption; mod.HasOptions |= group.IsOption;
var optionEnumerator = group switch var (files, swaps, manips) = group.GetCounts();
{ mod.TotalFileCount += files;
SingleModGroup single => single.OptionData, mod.TotalSwapCount += swaps;
MultiModGroup multi => multi.PrioritizedOptions.Select(o => o.Mod), mod.TotalManipulations += manips;
_ => [],
};
foreach (var s in optionEnumerator)
{
mod.TotalFileCount += s.Files.Count;
mod.TotalSwapCount += s.FileSwaps.Count;
mod.TotalManipulations += s.Manipulations.Count;
}
} }
} }

View file

@ -71,14 +71,14 @@ public static partial class ModMigration
foreach (var unusedFile in mod.FindUnusedFiles().Where(f => !seenMetaFiles.Contains(f))) foreach (var unusedFile in mod.FindUnusedFiles().Where(f => !seenMetaFiles.Contains(f)))
{ {
if (unusedFile.ToGamePath(mod.ModPath, out var gamePath) if (unusedFile.ToGamePath(mod.ModPath, out var gamePath)
&& !mod.Default.FileData.TryAdd(gamePath, unusedFile)) && !mod.Default.Files.TryAdd(gamePath, unusedFile))
Penumbra.Log.Error($"Could not add {gamePath} because it already points to {mod.Default.FileData[gamePath]}."); Penumbra.Log.Error($"Could not add {gamePath} because it already points to {mod.Default.Files[gamePath]}.");
} }
mod.Default.FileSwapData.Clear(); mod.Default.FileSwaps.Clear();
mod.Default.FileSwapData.EnsureCapacity(swaps.Count); mod.Default.FileSwaps.EnsureCapacity(swaps.Count);
foreach (var (gamePath, swapPath) in swaps) foreach (var (gamePath, swapPath) in swaps)
mod.Default.FileSwapData.Add(gamePath, swapPath); mod.Default.FileSwaps.Add(gamePath, swapPath);
creator.IncorporateMetaChanges(mod.Default, mod.ModPath, true); creator.IncorporateMetaChanges(mod.Default, mod.ModPath, true);
foreach (var (_, index) in mod.Groups.WithIndex()) foreach (var (_, index) in mod.Groups.WithIndex())
@ -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, newMultiGroup, option, seenMetaFiles), optionPriority++)); newMultiGroup.OptionData.Add(SubModFromOption(creator, mod, newMultiGroup, option, optionPriority++, seenMetaFiles));
break; break;
case GroupType.Single: case GroupType.Single:
@ -158,22 +158,41 @@ public static partial class ModMigration
} }
} }
private static void AddFilesToSubMod(SubMod mod, DirectoryInfo basePath, OptionV0 option, HashSet<FullPath> seenMetaFiles) private static void AddFilesToSubMod(IModDataContainer mod, DirectoryInfo basePath, OptionV0 option, HashSet<FullPath> seenMetaFiles)
{ {
foreach (var (relPath, gamePaths) in option.OptionFiles) foreach (var (relPath, gamePaths) in option.OptionFiles)
{ {
var fullPath = new FullPath(basePath, relPath); var fullPath = new FullPath(basePath, relPath);
foreach (var gamePath in gamePaths) foreach (var gamePath in gamePaths)
mod.FileData.TryAdd(gamePath, fullPath); mod.Files.TryAdd(gamePath, fullPath);
if (fullPath.Extension is ".meta" or ".rgsp") if (fullPath.Extension is ".meta" or ".rgsp")
seenMetaFiles.Add(fullPath); seenMetaFiles.Add(fullPath);
} }
} }
private static SubMod SubModFromOption(ModCreator creator, Mod mod, IModGroup group, OptionV0 option, HashSet<FullPath> seenMetaFiles) private static SingleSubMod SubModFromOption(ModCreator creator, Mod mod, SingleModGroup group, OptionV0 option,
HashSet<FullPath> seenMetaFiles)
{ {
var subMod = new SubMod(mod, group) { Name = option.OptionName }; var subMod = new SingleSubMod(mod, group)
{
Name = option.OptionName,
Description = option.OptionDesc,
};
AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles);
creator.IncorporateMetaChanges(subMod, mod.ModPath, false);
return subMod;
}
private static MultiSubMod SubModFromOption(ModCreator creator, Mod mod, MultiModGroup group, OptionV0 option,
ModPriority priority, HashSet<FullPath> seenMetaFiles)
{
var subMod = new MultiSubMod(mod, group)
{
Name = option.OptionName,
Description = option.OptionDesc,
Priority = priority,
};
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

@ -1,4 +1,3 @@
using System.Security.AccessControl;
using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.Notifications;
using OtterGui; using OtterGui;
using OtterGui.Classes; using OtterGui.Classes;
@ -179,10 +178,10 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS
switch (mod.Groups[groupIdx]) switch (mod.Groups[groupIdx])
{ {
case MultiModGroup multi: case MultiModGroup multi:
if (multi.PrioritizedOptions[optionIdx].Priority == newPriority) if (multi.OptionData[optionIdx].Priority == newPriority)
return; return;
multi.PrioritizedOptions[optionIdx] = (multi.PrioritizedOptions[optionIdx].Mod, newPriority); multi.OptionData[optionIdx].Priority = 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;
@ -213,70 +212,62 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS
} }
/// <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, int, bool) FindOrAddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue) public (IModOption, int, bool) FindOrAddOption(Mod mod, int groupIdx, string newName, SaveType saveType = SaveType.Queue)
{ {
var group = mod.Groups[groupIdx]; var group = mod.Groups[groupIdx];
switch (group) var idx = group.Options.IndexOf(o => o.Name == newName);
{ if (idx >= 0)
case SingleModGroup single: return (group.Options[idx], idx, false);
{
var idx = single.OptionData.IndexOf(o => o.Name == newName);
if (idx >= 0)
return (single.OptionData[idx], idx, false);
idx = single.AddOption(mod, newName); idx = group.AddOption(mod, newName);
if (idx < 0) 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}.");
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, idx, -1); communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1);
return (single.OptionData[^1], single.OptionData.Count - 1, true); return (group.Options[idx], idx, 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. </summary>
public void AddOption(Mod mod, int groupIdx, SubMod option) public void AddOption(Mod mod, int groupIdx, IModOption option)
=> AddOption(mod, groupIdx, option, ModPriority.Default);
/// <summary> Add an existing option to a given group with a given priority. </summary>
public void AddOption(Mod mod, int groupIdx, SubMod option, ModPriority priority)
{ {
var group = mod.Groups[groupIdx]; var group = mod.Groups[groupIdx];
int idx; int idx;
switch (group) switch (group)
{ {
case MultiModGroup { PrioritizedOptions.Count: >= IModGroup.MaxMultiOptions }: case MultiModGroup { OptionData.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:
{
idx = s.OptionData.Count; idx = s.OptionData.Count;
s.OptionData.Add(option); var newOption = new SingleSubMod(s.Mod, s)
{
Name = option.Name,
Description = option.Description,
};
if (option is IModDataContainer data)
IModDataContainer.Clone(data, newOption);
s.OptionData.Add(newOption);
break; break;
}
case MultiModGroup m: case MultiModGroup m:
idx = m.PrioritizedOptions.Count; {
m.PrioritizedOptions.Add((option, priority)); 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)
IModDataContainer.Clone(data, newOption);
m.OptionData.Add(newOption);
break; break;
default: }
return; default: return;
} }
saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
@ -295,7 +286,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS
break; break;
case MultiModGroup m: case MultiModGroup m:
m.PrioritizedOptions.RemoveAt(optionIdx); m.OptionData.RemoveAt(optionIdx);
break; break;
} }
@ -315,59 +306,59 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS
} }
/// <summary> Set the meta manipulations for a given option. Replaces existing manipulations. </summary> /// <summary> Set the meta manipulations for a given option. Replaces existing manipulations. </summary>
public void OptionSetManipulations(Mod mod, int groupIdx, int optionIdx, HashSet<MetaManipulation> manipulations, public void OptionSetManipulations(Mod mod, int groupIdx, int dataContainerIdx, HashSet<MetaManipulation> manipulations,
SaveType saveType = SaveType.Queue) SaveType saveType = SaveType.Queue)
{ {
var subMod = GetSubMod(mod, groupIdx, optionIdx); var subMod = GetSubMod(mod, groupIdx, dataContainerIdx);
if (subMod.Manipulations.Count == manipulations.Count if (subMod.Manipulations.Count == manipulations.Count
&& subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m))) && subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m)))
return; return;
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, dataContainerIdx, -1);
subMod.ManipulationData.SetTo(manipulations); subMod.Manipulations.SetTo(manipulations);
saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1); communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, dataContainerIdx, -1);
} }
/// <summary> Set the file redirections for a given option. Replaces existing redirections. </summary> /// <summary> Set the file redirections for a given option. Replaces existing redirections. </summary>
public void OptionSetFiles(Mod mod, int groupIdx, int optionIdx, IReadOnlyDictionary<Utf8GamePath, FullPath> replacements, public void OptionSetFiles(Mod mod, int groupIdx, int dataContainerIdx, IReadOnlyDictionary<Utf8GamePath, FullPath> replacements,
SaveType saveType = SaveType.Queue) SaveType saveType = SaveType.Queue)
{ {
var subMod = GetSubMod(mod, groupIdx, optionIdx); var subMod = GetSubMod(mod, groupIdx, dataContainerIdx);
if (subMod.FileData.SetEquals(replacements)) if (subMod.Files.SetEquals(replacements))
return; return;
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, dataContainerIdx, -1);
subMod.FileData.SetTo(replacements); subMod.Files.SetTo(replacements);
saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1); 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> /// <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 optionIdx, IReadOnlyDictionary<Utf8GamePath, FullPath> additions) public void OptionAddFiles(Mod mod, int groupIdx, int dataContainerIdx, IReadOnlyDictionary<Utf8GamePath, FullPath> additions)
{ {
var subMod = GetSubMod(mod, groupIdx, optionIdx); var subMod = GetSubMod(mod, groupIdx, dataContainerIdx);
var oldCount = subMod.FileData.Count; var oldCount = subMod.Files.Count;
subMod.FileData.AddFrom(additions); subMod.Files.AddFrom(additions);
if (oldCount != subMod.FileData.Count) if (oldCount != subMod.Files.Count)
{ {
saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, optionIdx, -1); communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, dataContainerIdx, -1);
} }
} }
/// <summary> Set the file swaps for a given option. Replaces existing swaps. </summary> /// <summary> Set the file swaps for a given option. Replaces existing swaps. </summary>
public void OptionSetFileSwaps(Mod mod, int groupIdx, int optionIdx, IReadOnlyDictionary<Utf8GamePath, FullPath> swaps, public void OptionSetFileSwaps(Mod mod, int groupIdx, int dataContainerIdx, IReadOnlyDictionary<Utf8GamePath, FullPath> swaps,
SaveType saveType = SaveType.Queue) SaveType saveType = SaveType.Queue)
{ {
var subMod = GetSubMod(mod, groupIdx, optionIdx); var subMod = GetSubMod(mod, groupIdx, dataContainerIdx);
if (subMod.FileSwapData.SetEquals(swaps)) if (subMod.FileSwaps.SetEquals(swaps))
return; return;
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, dataContainerIdx, -1);
subMod.FileSwapData.SetTo(swaps); subMod.FileSwaps.SetTo(swaps);
saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport)); saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1); communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, dataContainerIdx, -1);
} }
@ -389,17 +380,12 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS
} }
/// <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 IModDataContainer GetSubMod(Mod mod, int groupIdx, int dataContainerIdx)
{ {
if (groupIdx == -1 && optionIdx == 0) if (groupIdx == -1 && dataContainerIdx == 0)
return mod.Default; return mod.Default;
return mod.Groups[groupIdx] switch return mod.Groups[groupIdx].DataContainers[dataContainerIdx];
{
SingleModGroup s => s.OptionData[optionIdx],
MultiModGroup m => m.PrioritizedOptions[optionIdx].Mod,
_ => throw new InvalidOperationException(),
};
} }
} }

View file

@ -38,7 +38,7 @@ public sealed class Mod : IMod
internal Mod(DirectoryInfo modPath) internal Mod(DirectoryInfo modPath)
{ {
ModPath = modPath; ModPath = modPath;
Default = SubMod.CreateDefault(this); Default = new DefaultSubMod(this);
} }
public override string ToString() public override string ToString()
@ -61,8 +61,8 @@ public sealed class Mod : IMod
// Options // Options
public readonly SubMod Default; public readonly DefaultSubMod Default;
public readonly List<IModGroup> Groups = []; public readonly List<IModGroup> Groups = [];
public AppliedModData GetData(ModSettings? settings = null) public AppliedModData GetData(ModSettings? settings = null)
{ {
@ -77,21 +77,16 @@ public sealed class Mod : IMod
group.AddData(config, dictRedirections, setManips); group.AddData(config, dictRedirections, setManips);
} }
Default.AddData(dictRedirections, setManips); Default.AddDataTo(dictRedirections, setManips);
return new AppliedModData(dictRedirections, setManips); return new AppliedModData(dictRedirections, setManips);
} }
public IEnumerable<SubMod> AllSubMods public IEnumerable<IModDataContainer> AllDataContainers
=> Groups.SelectMany(o => o switch => Groups.SelectMany(o => o.DataContainers).Prepend(Default);
{
SingleModGroup single => single.OptionData,
MultiModGroup multi => multi.PrioritizedOptions.Select(s => s.Mod),
_ => [],
}).Prepend(Default);
public List<FullPath> FindUnusedFiles() public List<FullPath> FindUnusedFiles()
{ {
var modFiles = AllSubMods.SelectMany(o => o.Files) var modFiles = AllDataContainers.SelectMany(o => o.Files)
.Select(p => p.Value) .Select(p => p.Value)
.ToHashSet(); .ToHashSet();
return ModPath.EnumerateDirectories() return ModPath.EnumerateDirectories()

View file

@ -112,10 +112,8 @@ public partial class ModCreator(
var defaultFile = _saveService.FileNames.OptionGroupFile(mod, -1, Config.ReplaceNonAsciiOnImport); var defaultFile = _saveService.FileNames.OptionGroupFile(mod, -1, Config.ReplaceNonAsciiOnImport);
try try
{ {
if (!File.Exists(defaultFile)) var jObject = File.Exists(defaultFile) ? JObject.Parse(File.ReadAllText(defaultFile)) : new JObject();
mod.Default.Load(mod.ModPath, new JObject(), out _); IModDataContainer.Load(jObject, mod.Default, mod.ModPath);
else
mod.Default.Load(mod.ModPath, JObject.Parse(File.ReadAllText(defaultFile)), out _);
} }
catch (Exception e) catch (Exception e)
{ {
@ -154,7 +152,7 @@ public partial class ModCreator(
{ {
var changes = false; var changes = false;
List<string> deleteList = new(); List<string> deleteList = new();
foreach (var subMod in mod.AllSubMods) foreach (var subMod in mod.AllDataContainers)
{ {
var (localChanges, localDeleteList) = IncorporateMetaChanges(subMod, mod.ModPath, false); var (localChanges, localDeleteList) = IncorporateMetaChanges(subMod, mod.ModPath, false);
changes |= localChanges; changes |= localChanges;
@ -162,7 +160,7 @@ public partial class ModCreator(
deleteList.AddRange(localDeleteList); deleteList.AddRange(localDeleteList);
} }
SubMod.DeleteDeleteList(deleteList, delete); IModDataContainer.DeleteDeleteList(deleteList, delete);
if (!changes) if (!changes)
return; return;
@ -176,10 +174,10 @@ public partial class ModCreator(
/// If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod. /// If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod.
/// If delete is true, the files are deleted afterwards. /// If delete is true, the files are deleted afterwards.
/// </summary> /// </summary>
public (bool Changes, List<string> DeleteList) IncorporateMetaChanges(SubMod option, DirectoryInfo basePath, bool delete) public (bool Changes, List<string> DeleteList) IncorporateMetaChanges(IModDataContainer option, DirectoryInfo basePath, bool delete)
{ {
var deleteList = new List<string>(); var deleteList = new List<string>();
var oldSize = option.ManipulationData.Count; var oldSize = option.Manipulations.Count;
var deleteString = delete ? "with deletion." : "without deletion."; var deleteString = delete ? "with deletion." : "without deletion.";
foreach (var (key, file) in option.Files.ToList()) foreach (var (key, file) in option.Files.ToList())
{ {
@ -189,7 +187,7 @@ public partial class ModCreator(
{ {
if (ext1 == ".meta" || ext2 == ".meta") if (ext1 == ".meta" || ext2 == ".meta")
{ {
option.FileData.Remove(key); option.Files.Remove(key);
if (!file.Exists) if (!file.Exists)
continue; continue;
@ -198,11 +196,11 @@ public partial class ModCreator(
Penumbra.Log.Verbose( Penumbra.Log.Verbose(
$"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}"); $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}");
deleteList.Add(file.FullName); deleteList.Add(file.FullName);
option.ManipulationData.UnionWith(meta.MetaManipulations); option.Manipulations.UnionWith(meta.MetaManipulations);
} }
else if (ext1 == ".rgsp" || ext2 == ".rgsp") else if (ext1 == ".rgsp" || ext2 == ".rgsp")
{ {
option.FileData.Remove(key); option.Files.Remove(key);
if (!file.Exists) if (!file.Exists)
continue; continue;
@ -212,7 +210,7 @@ public partial class ModCreator(
$"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}"); $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}");
deleteList.Add(file.FullName); deleteList.Add(file.FullName);
option.ManipulationData.UnionWith(rgsp.MetaManipulations); option.Manipulations.UnionWith(rgsp.MetaManipulations);
} }
} }
catch (Exception e) catch (Exception e)
@ -221,8 +219,8 @@ public partial class ModCreator(
} }
} }
SubMod.DeleteDeleteList(deleteList, delete); IModDataContainer.DeleteDeleteList(deleteList, delete);
return (oldSize < option.ManipulationData.Count, deleteList); return (oldSize < option.Manipulations.Count, deleteList);
} }
/// <summary> /// <summary>
@ -238,7 +236,7 @@ public partial class ModCreator(
/// <summary> Create a file for an option group from given data. </summary> /// <summary> Create a file for an option group from given data. </summary>
public void CreateOptionGroup(DirectoryInfo baseFolder, GroupType type, string name, public void CreateOptionGroup(DirectoryInfo baseFolder, GroupType type, string name,
ModPriority priority, int index, Setting defaultSettings, string desc, IEnumerable<SubMod> subMods) ModPriority priority, int index, Setting defaultSettings, string desc, IEnumerable<MultiSubMod> subMods)
{ {
switch (type) switch (type)
{ {
@ -248,7 +246,7 @@ public partial class ModCreator(
group.Description = desc; group.Description = desc;
group.Priority = priority; group.Priority = priority;
group.DefaultSettings = defaultSettings; group.DefaultSettings = defaultSettings;
group.PrioritizedOptions.AddRange(subMods.Select((s, idx) => (s, new ModPriority(idx)))); group.OptionData.AddRange(subMods.Select(s => s.Clone(null!, group)));
_saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport));
break; break;
} }
@ -258,7 +256,7 @@ public partial class ModCreator(
group.Description = desc; group.Description = desc;
group.Priority = priority; group.Priority = priority;
group.DefaultSettings = defaultSettings; group.DefaultSettings = defaultSettings;
group.OptionData.AddRange(subMods); group.OptionData.AddRange(subMods.Select(s => s.ConvertToSingle(null!, group)));
_saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport)); _saveService.ImmediateSaveSync(new ModSaveGroup(baseFolder, group, index, Config.ReplaceNonAsciiOnImport));
break; break;
} }
@ -266,16 +264,15 @@ public partial class ModCreator(
} }
/// <summary> Create the data for a given sub mod from its data and the folder it is based on. </summary> /// <summary> Create the data for a given sub mod from its data and the folder it is based on. </summary>
public SubMod CreateSubMod(DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option) public MultiSubMod CreateSubMod(DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option, ModPriority priority)
{ {
var list = optionFolder.EnumerateNonHiddenFiles() var list = optionFolder.EnumerateNonHiddenFiles()
.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 = SubMod.CreateForSaving(option.Name); var mod = MultiSubMod.CreateForSaving(option.Name, option.Description, priority);
mod.Description = option.Description;
foreach (var (_, gamePath, file) in list) foreach (var (_, gamePath, file) in list)
mod.FileData.TryAdd(gamePath, file); mod.Files.TryAdd(gamePath, file);
IncorporateMetaChanges(mod, baseFolder, true); IncorporateMetaChanges(mod, baseFolder, true);
return mod; return mod;
@ -292,7 +289,7 @@ public partial class ModCreator(
foreach (var file in mod.FindUnusedFiles()) foreach (var file in mod.FindUnusedFiles())
{ {
if (Utf8GamePath.FromFile(new FileInfo(file.FullName), directory, out var gamePath, true)) if (Utf8GamePath.FromFile(new FileInfo(file.FullName), directory, out var gamePath, true))
mod.Default.FileData.TryAdd(gamePath, file); mod.Default.Files.TryAdd(gamePath, file);
} }
IncorporateMetaChanges(mod.Default, directory, true); IncorporateMetaChanges(mod.Default, directory, true);

View file

@ -1,12 +1,16 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor;
using Penumbra.String.Classes; using Penumbra.String.Classes;
namespace Penumbra.Mods.Subclasses; namespace Penumbra.Mods.Subclasses;
public interface IModDataContainer public interface IModDataContainer
{ {
public IMod Mod { get; }
public IModGroup? Group { get; }
public Dictionary<Utf8GamePath, FullPath> Files { get; set; } public Dictionary<Utf8GamePath, FullPath> Files { get; set; }
public Dictionary<Utf8GamePath, FullPath> FileSwaps { get; set; } public Dictionary<Utf8GamePath, FullPath> FileSwaps { get; set; }
public HashSet<MetaManipulation> Manipulations { get; set; } public HashSet<MetaManipulation> Manipulations { get; set; }
@ -21,6 +25,32 @@ public interface IModDataContainer
manipulations.UnionWith(Manipulations); manipulations.UnionWith(Manipulations);
} }
public string GetName()
=> this switch
{
IModOption o => o.FullName,
DefaultSubMod => DefaultSubMod.FullName,
_ => $"Container {GetDataIndices().DataIndex + 1}",
};
public string GetFullName()
=> this switch
{
IModOption o => o.FullName,
DefaultSubMod => DefaultSubMod.FullName,
_ when Group != null => $"{Group.Name}: Container {GetDataIndices().DataIndex + 1}",
_ => $"Container {GetDataIndices().DataIndex + 1}",
};
public static void Clone(IModDataContainer from, IModDataContainer to)
{
to.Files = new Dictionary<Utf8GamePath, FullPath>(from.Files);
to.FileSwaps = new Dictionary<Utf8GamePath, FullPath>(from.FileSwaps);
to.Manipulations = [.. from.Manipulations];
}
public (int GroupIndex, int DataIndex) GetDataIndices();
public static void Load(JToken json, IModDataContainer data, DirectoryInfo basePath) public static void Load(JToken json, IModDataContainer data, DirectoryInfo basePath)
{ {
data.Files.Clear(); data.Files.Clear();
@ -77,4 +107,22 @@ public interface IModDataContainer
serializer.Serialize(j, data.Manipulations); serializer.Serialize(j, data.Manipulations);
j.WriteEndObject(); j.WriteEndObject();
} }
internal static void DeleteDeleteList(IEnumerable<string> deleteList, bool delete)
{
if (!delete)
return;
foreach (var file in deleteList)
{
try
{
File.Delete(file);
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not delete incorporated meta file {file}:\n{e}");
}
}
}
} }

View file

@ -1,3 +1,4 @@
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Newtonsoft.Json; using Newtonsoft.Json;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
@ -6,6 +7,11 @@ using Penumbra.String.Classes;
namespace Penumbra.Mods.Subclasses; namespace Penumbra.Mods.Subclasses;
public interface ITexToolsGroup
{
public IReadOnlyList<IModDataOption> OptionData { get; }
}
public interface IModGroup public interface IModGroup
{ {
public const int MaxMultiOptions = 63; public const int MaxMultiOptions = 63;
@ -19,28 +25,89 @@ public interface IModGroup
public FullPath? FindBestMatch(Utf8GamePath gamePath); public FullPath? FindBestMatch(Utf8GamePath gamePath);
public int AddOption(Mod mod, string name, string description = ""); public int AddOption(Mod mod, string name, string description = "");
public bool ChangeOptionDescription(int optionIndex, string newDescription);
public bool ChangeOptionName(int optionIndex, string newName);
public IReadOnlyList<IModOption> Options { get; } public IReadOnlyList<IModOption> Options { get; }
public bool IsOption { get; } public IReadOnlyList<IModDataContainer> DataContainers { 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 int GetIndex();
public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations); public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations);
/// <summary> Ensure that a value is valid for a group. </summary> /// <summary> Ensure that a value is valid for a group. </summary>
public Setting FixSetting(Setting setting); public Setting FixSetting(Setting setting);
public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null);
public bool ChangeOptionDescription(int optionIndex, string newDescription)
{
if (optionIndex < 0 || optionIndex >= Options.Count)
return false;
var option = Options[optionIndex];
if (option.Description == newDescription)
return false;
option.Description = newDescription;
return true;
}
public bool ChangeOptionName(int optionIndex, string newName)
{
if (optionIndex < 0 || optionIndex >= Options.Count)
return false;
var option = Options[optionIndex];
if (option.Name == newName)
return false;
option.Name = newName;
return true;
}
public static void WriteJsonBase(JsonTextWriter jWriter, IModGroup group)
{
jWriter.WriteStartObject();
jWriter.WritePropertyName(nameof(group.Name));
jWriter.WriteValue(group!.Name);
jWriter.WritePropertyName(nameof(group.Description));
jWriter.WriteValue(group.Description);
jWriter.WritePropertyName(nameof(group.Priority));
jWriter.WriteValue(group.Priority.Value);
jWriter.WritePropertyName(nameof(group.Type));
jWriter.WriteValue(group.Type.ToString());
jWriter.WritePropertyName(nameof(group.DefaultSettings));
jWriter.WriteValue(group.DefaultSettings.Value);
}
public (int Redirections, int Swaps, int Manips) GetCounts();
public static (int Redirections, int Swaps, int Manips) GetCountsBase(IModGroup group)
{
var redirectionCount = 0;
var swapCount = 0;
var manipCount = 0;
foreach (var option in group.DataContainers)
{
redirectionCount += option.Files.Count;
swapCount += option.FileSwaps.Count;
manipCount += option.Manipulations.Count;
}
return (redirectionCount, swapCount, manipCount);
}
} }
public readonly struct ModSaveGroup : ISavable public readonly struct ModSaveGroup : ISavable
{ {
private readonly DirectoryInfo _basePath; private readonly DirectoryInfo _basePath;
private readonly IModGroup? _group; private readonly IModGroup? _group;
private readonly int _groupIdx; private readonly int _groupIdx;
private readonly SubMod? _defaultMod; private readonly DefaultSubMod? _defaultMod;
private readonly bool _onlyAscii; private readonly bool _onlyAscii;
public ModSaveGroup(Mod mod, int groupIdx, bool onlyAscii) public ModSaveGroup(Mod mod, int groupIdx, bool onlyAscii)
{ {
@ -61,7 +128,7 @@ public readonly struct ModSaveGroup : ISavable
_onlyAscii = onlyAscii; _onlyAscii = onlyAscii;
} }
public ModSaveGroup(DirectoryInfo basePath, SubMod @default, bool onlyAscii) public ModSaveGroup(DirectoryInfo basePath, DefaultSubMod @default, bool onlyAscii)
{ {
_basePath = basePath; _basePath = basePath;
_groupIdx = -1; _groupIdx = -1;
@ -77,42 +144,11 @@ public readonly struct ModSaveGroup : ISavable
using var j = new JsonTextWriter(writer); using var j = new JsonTextWriter(writer);
j.Formatting = Formatting.Indented; j.Formatting = Formatting.Indented;
var serializer = new JsonSerializer { Formatting = Formatting.Indented }; var serializer = new JsonSerializer { Formatting = Formatting.Indented };
j.WriteStartObject();
if (_groupIdx >= 0) if (_groupIdx >= 0)
{ _group!.WriteJson(j, serializer);
j.WriteStartObject();
j.WritePropertyName(nameof(_group.Name));
j.WriteValue(_group!.Name);
j.WritePropertyName(nameof(_group.Description));
j.WriteValue(_group.Description);
j.WritePropertyName(nameof(_group.Priority));
j.WriteValue(_group.Priority.Value);
j.WritePropertyName(nameof(Type));
j.WriteValue(_group.Type.ToString());
j.WritePropertyName(nameof(_group.DefaultSettings));
j.WriteValue(_group.DefaultSettings.Value);
switch (_group)
{
case SingleModGroup single:
j.WritePropertyName("Options");
j.WriteStartArray();
foreach (var option in single.OptionData)
SubMod.WriteSubMod(j, serializer, option, _basePath, null);
j.WriteEndArray();
j.WriteEndObject();
break;
case MultiModGroup multi:
j.WritePropertyName("Options");
j.WriteStartArray();
foreach (var (option, priority) in multi.PrioritizedOptions)
SubMod.WriteSubMod(j, serializer, option, _basePath, priority);
j.WriteEndArray();
j.WriteEndObject();
break;
}
}
else else
{ IModDataContainer.WriteModData(j, serializer, _defaultMod!, _basePath);
SubMod.WriteSubMod(j, serializer, _defaultMod!, _basePath, null); j.WriteEndObject();
}
} }
} }

View file

@ -15,6 +15,8 @@ public interface IModOption
option.Description = json[nameof(Description)]?.ToObject<string>() ?? string.Empty; option.Description = json[nameof(Description)]?.ToObject<string>() ?? string.Empty;
} }
public (int GroupIndex, int OptionIndex) GetOptionIndices();
public static void WriteModOption(JsonWriter j, IModOption option) public static void WriteModOption(JsonWriter j, IModOption option)
{ {
j.WritePropertyName(nameof(Name)); j.WritePropertyName(nameof(Name));

View file

@ -1,4 +1,5 @@
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;
@ -10,20 +11,30 @@ 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(Mod mod) : IModGroup public sealed class MultiModGroup(Mod mod) : IModGroup, ITexToolsGroup
{ {
public GroupType Type public GroupType Type
=> GroupType.Multi; => GroupType.Multi;
public Mod Mod { get; set; } = mod; 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; }
public Setting DefaultSettings { get; set; } public Setting DefaultSettings { get; set; }
public readonly List<MultiSubMod> OptionData = [];
public IReadOnlyList<IModOption> Options
=> OptionData;
public IReadOnlyList<IModDataContainer> DataContainers
=> OptionData;
public bool IsOption
=> OptionData.Count > 0;
public FullPath? FindBestMatch(Utf8GamePath gamePath) public FullPath? FindBestMatch(Utf8GamePath gamePath)
=> PrioritizedOptions.OrderByDescending(o => o.Priority) => OptionData.OrderByDescending(o => o.Priority)
.SelectWhere(o => (o.Mod.FileData.TryGetValue(gamePath, out var file) || o.Mod.FileSwapData.TryGetValue(gamePath, out file), file)) .SelectWhere(o => (o.Files.TryGetValue(gamePath, out var file) || o.FileSwaps.TryGetValue(gamePath, out file), file))
.FirstOrDefault(); .FirstOrDefault();
public int AddOption(Mod mod, string name, string description = "") public int AddOption(Mod mod, string name, string description = "")
@ -32,49 +43,15 @@ public sealed class MultiModGroup(Mod mod) : IModGroup
if (groupIdx < 0) if (groupIdx < 0)
return -1; return -1;
var subMod = new SubMod(mod, this) var subMod = new MultiSubMod(mod, this)
{ {
Name = name, Name = name,
Description = description, Description = description,
}; };
PrioritizedOptions.Add((subMod, ModPriority.Default)); OptionData.Add(subMod);
return PrioritizedOptions.Count - 1; return OptionData.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
=> PrioritizedOptions.Count > 0;
public readonly List<(SubMod Mod, ModPriority Priority)> PrioritizedOptions = [];
public static MultiModGroup? Load(Mod mod, JObject json, int groupIdx) public static MultiModGroup? Load(Mod mod, JObject json, int groupIdx)
{ {
var ret = new MultiModGroup(mod) var ret = new MultiModGroup(mod)
@ -91,7 +68,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup
if (options != null) if (options != null)
foreach (var child in options.Children()) foreach (var child in options.Children())
{ {
if (ret.PrioritizedOptions.Count == IModGroup.MaxMultiOptions) if (ret.OptionData.Count == IModGroup.MaxMultiOptions)
{ {
Penumbra.Messager.NotificationMessage( Penumbra.Messager.NotificationMessage(
$"Multi Group {ret.Name} in {mod.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options.", $"Multi Group {ret.Name} in {mod.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options.",
@ -99,9 +76,8 @@ public sealed class MultiModGroup(Mod mod) : IModGroup
break; break;
} }
var subMod = new SubMod(mod, ret); var subMod = new MultiSubMod(mod, ret, child);
subMod.Load(mod.ModPath, child, out var priority); ret.OptionData.Add(subMod);
ret.PrioritizedOptions.Add((subMod, priority));
} }
ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings); ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings);
@ -115,39 +91,68 @@ public sealed class MultiModGroup(Mod mod) : IModGroup
{ {
case GroupType.Multi: return this; case GroupType.Multi: return this;
case GroupType.Single: case GroupType.Single:
var multi = new SingleModGroup(Mod) var single = new SingleModGroup(Mod)
{ {
Name = Name, Name = Name,
Description = Description, Description = Description,
Priority = Priority, Priority = Priority,
DefaultSettings = DefaultSettings.TurnMulti(PrioritizedOptions.Count), DefaultSettings = DefaultSettings.TurnMulti(OptionData.Count),
}; };
multi.OptionData.AddRange(PrioritizedOptions.Select(p => p.Mod)); single.OptionData.AddRange(OptionData.Select(o => o.ConvertToSingle(Mod, single)));
return multi; return single;
default: throw new ArgumentOutOfRangeException(nameof(type), type, null); default: throw new ArgumentOutOfRangeException(nameof(type), type, null);
} }
} }
public bool MoveOption(int optionIdxFrom, int optionIdxTo) public bool MoveOption(int optionIdxFrom, int optionIdxTo)
{ {
if (!PrioritizedOptions.Move(optionIdxFrom, optionIdxTo)) if (!OptionData.Move(optionIdxFrom, optionIdxTo))
return false; return false;
DefaultSettings = DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo); DefaultSettings = DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo);
return true; 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 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 OptionData.WithIndex().OrderByDescending(o => o.Value.Priority))
{ {
if (setting.HasFlag(index)) if (setting.HasFlag(index))
option.Mod.AddData(redirections, manipulations); option.AddDataTo(redirections, manipulations);
} }
} }
public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null)
{
IModGroup.WriteJsonBase(jWriter, this);
jWriter.WritePropertyName("Options");
jWriter.WriteStartArray();
foreach (var option in OptionData)
{
IModOption.WriteModOption(jWriter, option);
jWriter.WritePropertyName(nameof(option.Priority));
jWriter.WriteValue(option.Priority.Value);
IModDataContainer.WriteModData(jWriter, serializer, option, basePath ?? Mod.ModPath);
}
jWriter.WriteEndArray();
jWriter.WriteEndObject();
}
public (int Redirections, int Swaps, int Manips) GetCounts()
=> IModGroup.GetCountsBase(this);
public Setting FixSetting(Setting setting) public Setting FixSetting(Setting setting)
=> new(setting.Value & ((1ul << PrioritizedOptions.Count) - 1)); => new(setting.Value & ((1ul << OptionData.Count) - 1));
/// <summary> Create a group without a mod only for saving it in the creator. </summary> /// <summary> Create a group without a mod only for saving it in the creator. </summary>
internal static MultiModGroup CreateForSaving(string name) internal static MultiModGroup CreateForSaving(string name)
@ -155,4 +160,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup
{ {
Name = name, Name = name,
}; };
IReadOnlyList<IModDataOption> ITexToolsGroup.OptionData
=> OptionData;
} }

View file

@ -1,3 +1,4 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using OtterGui; using OtterGui;
using OtterGui.Filesystem; using OtterGui.Filesystem;
@ -8,7 +9,7 @@ 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(Mod mod) : IModGroup public sealed class SingleModGroup(Mod mod) : IModGroup, ITexToolsGroup
{ {
public GroupType Type public GroupType Type
=> GroupType.Single; => GroupType.Single;
@ -19,16 +20,19 @@ public sealed class SingleModGroup(Mod mod) : IModGroup
public ModPriority Priority { get; set; } public ModPriority Priority { get; set; }
public Setting DefaultSettings { get; set; } public Setting DefaultSettings { get; set; }
public readonly List<SubMod> OptionData = []; public readonly List<SingleSubMod> OptionData = [];
IReadOnlyList<IModDataOption> ITexToolsGroup.OptionData
=> OptionData;
public FullPath? FindBestMatch(Utf8GamePath gamePath) public FullPath? FindBestMatch(Utf8GamePath gamePath)
=> OptionData => OptionData
.SelectWhere(m => (m.FileData.TryGetValue(gamePath, out var file) || m.FileSwapData.TryGetValue(gamePath, out file), file)) .SelectWhere(m => (m.Files.TryGetValue(gamePath, out var file) || m.FileSwaps.TryGetValue(gamePath, out file), file))
.FirstOrDefault(); .FirstOrDefault();
public int AddOption(Mod mod, string name, string description = "") public int AddOption(Mod mod, string name, string description = "")
{ {
var subMod = new SubMod(mod, this) var subMod = new SingleSubMod(mod, this)
{ {
Name = name, Name = name,
Description = description, Description = description,
@ -37,35 +41,12 @@ public sealed class SingleModGroup(Mod mod) : IModGroup
return OptionData.Count - 1; 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 public IReadOnlyList<IModOption> Options
=> OptionData; => OptionData;
public IReadOnlyList<IModDataContainer> DataContainers
=> OptionData;
public bool IsOption public bool IsOption
=> OptionData.Count > 1; => OptionData.Count > 1;
@ -85,8 +66,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup
if (options != null) if (options != null)
foreach (var child in options.Children()) foreach (var child in options.Children())
{ {
var subMod = new SubMod(mod, ret); var subMod = new SingleSubMod(mod, ret, child);
subMod.Load(mod.ModPath, child, out _);
ret.OptionData.Add(subMod); ret.OptionData.Add(subMod);
} }
@ -107,7 +87,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup
Priority = Priority, Priority = Priority,
DefaultSettings = Setting.Multi((int)DefaultSettings.Value), DefaultSettings = Setting.Multi((int)DefaultSettings.Value),
}; };
multi.PrioritizedOptions.AddRange(OptionData.Select((o, i) => (o, new ModPriority(i)))); multi.OptionData.AddRange(OptionData.Select((o, i) => o.ConvertToMulti(Mod, multi, new ModPriority(i))));
return multi; return multi;
default: throw new ArgumentOutOfRangeException(nameof(type), type, null); default: throw new ArgumentOutOfRangeException(nameof(type), type, null);
} }
@ -137,12 +117,39 @@ public sealed class SingleModGroup(Mod mod) : IModGroup
return true; 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 void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations) public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations)
=> OptionData[setting.AsIndex].AddData(redirections, manipulations); => OptionData[setting.AsIndex].AddDataTo(redirections, manipulations);
public Setting FixSetting(Setting setting) public Setting FixSetting(Setting setting)
=> OptionData.Count == 0 ? Setting.Zero : new Setting(Math.Min(setting.Value, (ulong)(OptionData.Count - 1))); => OptionData.Count == 0 ? Setting.Zero : new Setting(Math.Min(setting.Value, (ulong)(OptionData.Count - 1)));
public (int Redirections, int Swaps, int Manips) GetCounts()
=> IModGroup.GetCountsBase(this);
public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null)
{
IModGroup.WriteJsonBase(jWriter, this);
jWriter.WritePropertyName("Options");
jWriter.WriteStartArray();
foreach (var option in OptionData)
{
IModOption.WriteModOption(jWriter, option);
IModDataContainer.WriteModData(jWriter, serializer, option, basePath ?? Mod.ModPath);
}
jWriter.WriteEndArray();
jWriter.WriteEndObject();
}
/// <summary> Create a group without a mod only for saving it in the creator. </summary> /// <summary> Create a group without a mod only for saving it in the creator. </summary>
internal static SingleModGroup CreateForSaving(string name) internal static SingleModGroup CreateForSaving(string name)
=> new(null!) => new(null!)

View file

@ -7,7 +7,9 @@ using Penumbra.String.Classes;
namespace Penumbra.Mods.Subclasses; namespace Penumbra.Mods.Subclasses;
public class SingleSubMod(Mod mod, SingleModGroup group) : IModOption, IModDataContainer public interface IModDataOption : IModOption, IModDataContainer;
public class SingleSubMod(Mod mod, SingleModGroup group) : IModDataOption
{ {
internal readonly Mod Mod = mod; internal readonly Mod Mod = mod;
internal readonly SingleModGroup Group = group; internal readonly SingleModGroup Group = group;
@ -19,12 +21,68 @@ public class SingleSubMod(Mod mod, SingleModGroup group) : IModOption, IModDataC
public string Description { get; set; } = string.Empty; public string Description { get; set; } = string.Empty;
IMod IModDataContainer.Mod
=> Mod;
IModGroup IModDataContainer.Group
=> Group;
public Dictionary<Utf8GamePath, FullPath> Files { get; set; } = []; public Dictionary<Utf8GamePath, FullPath> Files { get; set; } = [];
public Dictionary<Utf8GamePath, FullPath> FileSwaps { get; set; } = []; public Dictionary<Utf8GamePath, FullPath> FileSwaps { get; set; } = [];
public HashSet<MetaManipulation> Manipulations { get; set; } = []; public HashSet<MetaManipulation> Manipulations { get; set; } = [];
public SingleSubMod(Mod mod, SingleModGroup group, JToken json)
: this(mod, group)
{
IModOption.Load(json, this);
IModDataContainer.Load(json, this, mod.ModPath);
}
public SingleSubMod Clone(Mod mod, SingleModGroup group)
{
var ret = new SingleSubMod(mod, group)
{
Name = Name,
Description = Description,
};
IModDataContainer.Clone(this, ret);
return ret;
}
public MultiSubMod ConvertToMulti(Mod mod, MultiModGroup group, ModPriority priority)
{
var ret = new MultiSubMod(mod, group)
{
Name = Name,
Description = Description,
Priority = priority,
};
IModDataContainer.Clone(this, ret);
return ret;
}
public void AddDataTo(Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations)
=> ((IModDataContainer)this).AddDataTo(redirections, manipulations);
public (int GroupIndex, int DataIndex) GetDataIndices()
=> (Group.GetIndex(), GetDataIndex());
public (int GroupIndex, int OptionIndex) GetOptionIndices()
=> (Group.GetIndex(), GetDataIndex());
private int GetDataIndex()
{
var dataIndex = Group.DataContainers.IndexOf(this);
if (dataIndex < 0)
throw new Exception($"Group {Group.Name} from SubMod {Name} does not contain this SubMod.");
return dataIndex;
}
} }
public class MultiSubMod(Mod mod, MultiModGroup group) : IModOption, IModDataContainer public class MultiSubMod(Mod mod, MultiModGroup group) : IModDataOption
{ {
internal readonly Mod Mod = mod; internal readonly Mod Mod = mod;
internal readonly MultiModGroup Group = group; internal readonly MultiModGroup Group = group;
@ -40,12 +98,76 @@ public class MultiSubMod(Mod mod, MultiModGroup group) : IModOption, IModDataCon
public Dictionary<Utf8GamePath, FullPath> Files { get; set; } = []; public Dictionary<Utf8GamePath, FullPath> Files { get; set; } = [];
public Dictionary<Utf8GamePath, FullPath> FileSwaps { get; set; } = []; public Dictionary<Utf8GamePath, FullPath> FileSwaps { get; set; } = [];
public HashSet<MetaManipulation> Manipulations { get; set; } = []; public HashSet<MetaManipulation> Manipulations { get; set; } = [];
IMod IModDataContainer.Mod
=> Mod;
IModGroup IModDataContainer.Group
=> Group;
public MultiSubMod(Mod mod, MultiModGroup group, JToken json)
: this(mod, group)
{
IModOption.Load(json, this);
IModDataContainer.Load(json, this, mod.ModPath);
Priority = json[nameof(IModGroup.Priority)]?.ToObject<ModPriority>() ?? ModPriority.Default;
}
public MultiSubMod Clone(Mod mod, MultiModGroup group)
{
var ret = new MultiSubMod(mod, group)
{
Name = Name,
Description = Description,
Priority = Priority,
};
IModDataContainer.Clone(this, ret);
return ret;
}
public SingleSubMod ConvertToSingle(Mod mod, SingleModGroup group)
{
var ret = new SingleSubMod(mod, group)
{
Name = Name,
Description = Description,
};
IModDataContainer.Clone(this, ret);
return ret;
}
public void AddDataTo(Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations)
=> ((IModDataContainer)this).AddDataTo(redirections, manipulations);
public static MultiSubMod CreateForSaving(string name, string description, ModPriority priority)
=> new(null!, null!)
{
Name = name,
Description = description,
Priority = priority,
};
public (int GroupIndex, int DataIndex) GetDataIndices()
=> (Group.GetIndex(), GetDataIndex());
public (int GroupIndex, int OptionIndex) GetOptionIndices()
=> (Group.GetIndex(), GetDataIndex());
private int GetDataIndex()
{
var dataIndex = Group.DataContainers.IndexOf(this);
if (dataIndex < 0)
throw new Exception($"Group {Group.Name} from SubMod {Name} does not contain this SubMod.");
return dataIndex;
}
} }
public class DefaultSubMod(IMod mod) : IModDataContainer public class DefaultSubMod(IMod mod) : IModDataContainer
{ {
public string FullName public const string FullName = "Default Option";
=> "Default Option";
public string Description public string Description
=> string.Empty; => string.Empty;
@ -55,183 +177,176 @@ public class DefaultSubMod(IMod mod) : IModDataContainer
public Dictionary<Utf8GamePath, FullPath> Files { get; set; } = []; public Dictionary<Utf8GamePath, FullPath> Files { get; set; } = [];
public Dictionary<Utf8GamePath, FullPath> FileSwaps { get; set; } = []; public Dictionary<Utf8GamePath, FullPath> FileSwaps { get; set; } = [];
public HashSet<MetaManipulation> Manipulations { get; set; } = []; public HashSet<MetaManipulation> Manipulations { get; set; } = [];
IMod IModDataContainer.Mod
=> Mod;
IModGroup? IModDataContainer.Group
=> null;
public DefaultSubMod(Mod mod, JToken json)
: this(mod)
{
IModDataContainer.Load(json, this, mod.ModPath);
}
public void AddDataTo(Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations)
=> ((IModDataContainer)this).AddDataTo(redirections, manipulations);
public (int GroupIndex, int DataIndex) GetDataIndices()
=> (-1, 0);
} }
/// <summary>
/// A sub mod is a collection of
/// - file replacements
/// - file swaps
/// - meta manipulations
/// that can be used either as an option or as the default data for a mod.
/// It can be loaded and reloaded from Json.
/// 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.
/// </summary>
public sealed class SubMod(IMod mod, IModGroup group) : IModOption
{
public string Name { get; set; } = "Default";
public string FullName //public sealed class SubMod(IMod mod, IModGroup group) : IModOption
=> Group == null ? "Default Option" : $"{Group.Name}: {Name}"; //{
// public string Name { get; set; } = "Default";
public string Description { get; set; } = string.Empty; //
// public string FullName
internal readonly IMod Mod = mod; // => Group == null ? "Default Option" : $"{Group.Name}: {Name}";
internal readonly IModGroup? Group = group; //
// public string Description { get; set; } = string.Empty;
internal (int GroupIdx, int OptionIdx) GetIndices() //
{ // internal readonly IMod Mod = mod;
if (IsDefault) // internal readonly IModGroup? Group = group;
return (-1, 0); //
// internal (int GroupIdx, int OptionIdx) GetIndices()
var groupIdx = Mod.Groups.IndexOf(Group); // {
if (groupIdx < 0) // if (IsDefault)
throw new Exception($"Group {Group.Name} from SubMod {Name} is not contained in Mod {Mod.Name}."); // return (-1, 0);
//
return (groupIdx, GetOptionIndex()); // 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}.");
private int GetOptionIndex() //
{ // return (groupIdx, GetOptionIndex());
var optionIndex = Group switch // }
{ //
null => 0, // private int GetOptionIndex()
SingleModGroup single => single.OptionData.IndexOf(this), // {
MultiModGroup multi => multi.PrioritizedOptions.IndexOf(p => p.Mod == this), // var optionIndex = Group switch
_ => throw new Exception($"Group {Group.Name} from SubMod {Name} has unknown type {typeof(Group)}"), // {
}; // null => 0,
if (optionIndex < 0) // SingleModGroup single => single.OptionData.IndexOf(this),
throw new Exception($"Group {Group!.Name} from SubMod {Name} does not contain this SubMod."); // MultiModGroup multi => multi.OptionData.IndexOf(p => p.Mod == this),
// _ => throw new Exception($"Group {Group.Name} from SubMod {Name} has unknown type {typeof(Group)}"),
return optionIndex; // };
} // if (optionIndex < 0)
// throw new Exception($"Group {Group!.Name} from SubMod {Name} does not contain this SubMod.");
public static SubMod CreateDefault(IMod mod) //
=> new(mod, null!); // return optionIndex;
// }
[MemberNotNullWhen(false, nameof(Group))] //
public bool IsDefault // public static SubMod CreateDefault(IMod mod)
=> Group == null; // => new(mod, null!);
//
public void AddData(Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations) // [MemberNotNullWhen(false, nameof(Group))]
{ // public bool IsDefault
foreach (var (path, file) in Files) // => Group == null;
redirections.TryAdd(path, file); //
// public void AddData(Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations)
foreach (var (path, file) in FileSwaps) // {
redirections.TryAdd(path, file); // foreach (var (path, file) in Files)
manipulations.UnionWith(Manipulations); // redirections.TryAdd(path, file);
} //
// foreach (var (path, file) in FileSwaps)
public Dictionary<Utf8GamePath, FullPath> FileData = []; // redirections.TryAdd(path, file);
public Dictionary<Utf8GamePath, FullPath> FileSwapData = []; // manipulations.UnionWith(Manipulations);
public HashSet<MetaManipulation> ManipulationData = []; // }
//
public IReadOnlyDictionary<Utf8GamePath, FullPath> Files // public Dictionary<Utf8GamePath, FullPath> FileData = [];
=> FileData; // public Dictionary<Utf8GamePath, FullPath> FileSwapData = [];
// public HashSet<MetaManipulation> ManipulationData = [];
public IReadOnlyDictionary<Utf8GamePath, FullPath> FileSwaps //
=> FileSwapData; // public IReadOnlyDictionary<Utf8GamePath, FullPath> Files
// => FileData;
public IReadOnlySet<MetaManipulation> Manipulations //
=> ManipulationData; // public IReadOnlyDictionary<Utf8GamePath, FullPath> FileSwaps
// => FileSwapData;
public void Load(DirectoryInfo basePath, JToken json, out ModPriority priority) //
{ // public IReadOnlySet<MetaManipulation> Manipulations
FileData.Clear(); // => ManipulationData;
FileSwapData.Clear(); //
ManipulationData.Clear(); // public void Load(DirectoryInfo basePath, JToken json, out ModPriority priority)
// {
// Every option has a name, but priorities are only relevant for multi group options. // FileData.Clear();
Name = json[nameof(Name)]?.ToObject<string>() ?? string.Empty; // FileSwapData.Clear();
Description = json[nameof(Description)]?.ToObject<string>() ?? string.Empty; // ManipulationData.Clear();
priority = json[nameof(IModGroup.Priority)]?.ToObject<ModPriority>() ?? ModPriority.Default; //
// // Every option has a name, but priorities are only relevant for multi group options.
var files = (JObject?)json[nameof(Files)]; // Name = json[nameof(Name)]?.ToObject<string>() ?? string.Empty;
if (files != null) // Description = json[nameof(Description)]?.ToObject<string>() ?? string.Empty;
foreach (var property in files.Properties()) // priority = json[nameof(IModGroup.Priority)]?.ToObject<ModPriority>() ?? ModPriority.Default;
{ //
if (Utf8GamePath.FromString(property.Name, out var p, true)) // var files = (JObject?)json[nameof(Files)];
FileData.TryAdd(p, new FullPath(basePath, property.Value.ToObject<Utf8RelPath>())); // if (files != null)
} // foreach (var property in files.Properties())
// {
var swaps = (JObject?)json[nameof(FileSwaps)]; // if (Utf8GamePath.FromString(property.Name, out var p, true))
if (swaps != null) // FileData.TryAdd(p, new FullPath(basePath, property.Value.ToObject<Utf8RelPath>()));
foreach (var property in swaps.Properties()) // }
{ //
if (Utf8GamePath.FromString(property.Name, out var p, true)) // var swaps = (JObject?)json[nameof(FileSwaps)];
FileSwapData.TryAdd(p, new FullPath(property.Value.ToObject<string>()!)); // if (swaps != null)
} // foreach (var property in swaps.Properties())
// {
var manips = json[nameof(Manipulations)]; // if (Utf8GamePath.FromString(property.Name, out var p, true))
if (manips != null) // FileSwapData.TryAdd(p, new FullPath(property.Value.ToObject<string>()!));
foreach (var s in manips.Children().Select(c => c.ToObject<MetaManipulation>()) // }
.Where(m => m.Validate())) //
ManipulationData.Add(s); // var manips = json[nameof(Manipulations)];
} // if (manips != null)
// foreach (var s in manips.Children().Select(c => c.ToObject<MetaManipulation>())
internal static void DeleteDeleteList(IEnumerable<string> deleteList, bool delete) // .Where(m => m.Validate()))
{ // ManipulationData.Add(s);
if (!delete) // }
return; //
//
foreach (var file in deleteList) // /// <summary> Create a sub mod without a mod or group only for saving it in the creator. </summary>
{ // internal static SubMod CreateForSaving(string name)
try // => new(null!, null!)
{ // {
File.Delete(file); // Name = name,
} // };
catch (Exception e) //
{ //
Penumbra.Log.Error($"Could not delete incorporated meta file {file}:\n{e}"); // public static void WriteSubMod(JsonWriter j, JsonSerializer serializer, SubMod mod, DirectoryInfo basePath, ModPriority? priority)
} // {
} // j.WriteStartObject();
} // j.WritePropertyName(nameof(Name));
// j.WriteValue(mod.Name);
/// <summary> Create a sub mod without a mod or group only for saving it in the creator. </summary> // j.WritePropertyName(nameof(Description));
internal static SubMod CreateForSaving(string name) // j.WriteValue(mod.Description);
=> new(null!, null!) // if (priority != null)
{ // {
Name = name, // j.WritePropertyName(nameof(IModGroup.Priority));
}; // j.WriteValue(priority.Value.Value);
// }
//
public static void WriteSubMod(JsonWriter j, JsonSerializer serializer, SubMod mod, DirectoryInfo basePath, ModPriority? priority) // j.WritePropertyName(nameof(mod.Files));
{ // j.WriteStartObject();
j.WriteStartObject(); // foreach (var (gamePath, file) in mod.Files)
j.WritePropertyName(nameof(Name)); // {
j.WriteValue(mod.Name); // if (file.ToRelPath(basePath, out var relPath))
j.WritePropertyName(nameof(Description)); // {
j.WriteValue(mod.Description); // j.WritePropertyName(gamePath.ToString());
if (priority != null) // j.WriteValue(relPath.ToString());
{ // }
j.WritePropertyName(nameof(IModGroup.Priority)); // }
j.WriteValue(priority.Value.Value); //
} // j.WriteEndObject();
// j.WritePropertyName(nameof(mod.FileSwaps));
j.WritePropertyName(nameof(mod.Files)); // j.WriteStartObject();
j.WriteStartObject(); // foreach (var (gamePath, file) in mod.FileSwaps)
foreach (var (gamePath, file) in mod.Files) // {
{ // j.WritePropertyName(gamePath.ToString());
if (file.ToRelPath(basePath, out var relPath)) // j.WriteValue(file.ToString());
{ // }
j.WritePropertyName(gamePath.ToString()); //
j.WriteValue(relPath.ToString()); // j.WriteEndObject();
} // j.WritePropertyName(nameof(mod.Manipulations));
} // serializer.Serialize(j, mod.Manipulations);
// j.WriteEndObject();
j.WriteEndObject(); // }
j.WritePropertyName(nameof(mod.FileSwaps)); //}
j.WriteStartObject();
foreach (var (gamePath, file) in mod.FileSwaps)
{
j.WritePropertyName(gamePath.ToString());
j.WriteValue(file.ToString());
}
j.WriteEndObject();
j.WritePropertyName(nameof(mod.Manipulations));
serializer.Serialize(j, mod.Manipulations);
j.WriteEndObject();
}
}

View file

@ -18,49 +18,46 @@ public class TemporaryMod : IMod
public int TotalManipulations public int TotalManipulations
=> Default.Manipulations.Count; => Default.Manipulations.Count;
public readonly SubMod Default; public readonly DefaultSubMod Default;
public AppliedModData GetData(ModSettings? settings = null) public AppliedModData GetData(ModSettings? settings = null)
{ {
Dictionary<Utf8GamePath, FullPath> dict; Dictionary<Utf8GamePath, FullPath> dict;
if (Default.FileSwapData.Count == 0) if (Default.FileSwaps.Count == 0)
{ {
dict = Default.FileData; dict = Default.Files;
} }
else if (Default.FileData.Count == 0) else if (Default.Files.Count == 0)
{ {
dict = Default.FileSwapData; dict = Default.FileSwaps;
} }
else else
{ {
// Need to ensure uniqueness. // Need to ensure uniqueness.
dict = new Dictionary<Utf8GamePath, FullPath>(Default.FileData.Count + Default.FileSwaps.Count); dict = new Dictionary<Utf8GamePath, FullPath>(Default.Files.Count + Default.FileSwaps.Count);
foreach (var (gamePath, file) in Default.FileData.Concat(Default.FileSwaps)) foreach (var (gamePath, file) in Default.Files.Concat(Default.FileSwaps))
dict.TryAdd(gamePath, file); dict.TryAdd(gamePath, file);
} }
return new AppliedModData(dict, Default.ManipulationData); return new AppliedModData(dict, Default.Manipulations);
} }
public IReadOnlyList<IModGroup> Groups public IReadOnlyList<IModGroup> Groups
=> Array.Empty<IModGroup>(); => Array.Empty<IModGroup>();
public IEnumerable<SubMod> AllSubMods
=> [Default];
public TemporaryMod() public TemporaryMod()
=> Default = SubMod.CreateDefault(this); => Default = new(this);
public void SetFile(Utf8GamePath gamePath, FullPath fullPath) public void SetFile(Utf8GamePath gamePath, FullPath fullPath)
=> Default.FileData[gamePath] = fullPath; => Default.Files[gamePath] = fullPath;
public bool SetManipulation(MetaManipulation manip) public bool SetManipulation(MetaManipulation manip)
=> Default.ManipulationData.Remove(manip) | Default.ManipulationData.Add(manip); => Default.Manipulations.Remove(manip) | Default.Manipulations.Add(manip);
public void SetAll(Dictionary<Utf8GamePath, FullPath> dict, HashSet<MetaManipulation> manips) public void SetAll(Dictionary<Utf8GamePath, FullPath> dict, HashSet<MetaManipulation> manips)
{ {
Default.FileData = dict; Default.Files = dict;
Default.ManipulationData = manips; Default.Manipulations = manips;
} }
public static void SaveTempCollection(Configuration config, SaveService saveService, ModManager modManager, ModCollection collection, public static void SaveTempCollection(Configuration config, SaveService saveService, ModManager modManager, ModCollection collection,
@ -93,16 +90,16 @@ public class TemporaryMod : IMod
{ {
var target = Path.Combine(fileDir.FullName, Path.GetFileName(targetPath)); var target = Path.Combine(fileDir.FullName, Path.GetFileName(targetPath));
File.Copy(targetPath, target, true); File.Copy(targetPath, target, true);
defaultMod.FileData[gamePath] = new FullPath(target); defaultMod.Files[gamePath] = new FullPath(target);
} }
else else
{ {
defaultMod.FileSwapData[gamePath] = new FullPath(targetPath); defaultMod.FileSwaps[gamePath] = new FullPath(targetPath);
} }
} }
foreach (var manip in collection.MetaCache?.Manipulations ?? Array.Empty<MetaManipulation>()) foreach (var manip in collection.MetaCache?.Manipulations ?? Array.Empty<MetaManipulation>())
defaultMod.ManipulationData.Add(manip); defaultMod.Manipulations.Add(manip);
saveService.ImmediateSave(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport)); saveService.ImmediateSave(new ModSaveGroup(dir, defaultMod, config.ReplaceNonAsciiOnImport));
modManager.AddMod(dir); modManager.AddMod(dir);

View file

@ -305,7 +305,7 @@ public class FileEditor<T>(
UiHelpers.Text(gamePath.Path); UiHelpers.Text(gamePath.Path);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value()); using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value());
ImGui.TextUnformatted(option.FullName); ImGui.TextUnformatted(option.GetFullName());
} }
} }

View file

@ -3,7 +3,6 @@ using ImGuiNET;
using OtterGui; using OtterGui;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Raii; using OtterGui.Raii;
using Penumbra.Mods;
using Penumbra.Mods.Editor; using Penumbra.Mods.Editor;
using Penumbra.Mods.Subclasses; using Penumbra.Mods.Subclasses;
using Penumbra.String.Classes; using Penumbra.String.Classes;
@ -79,7 +78,7 @@ public partial class ModEditWindow
var file = f.RelPath.ToString(); var file = f.RelPath.ToString();
return f.SubModUsage.Count == 0 return f.SubModUsage.Count == 0
? Enumerable.Repeat((file, "Unused", string.Empty, 0x40000080u), 1) ? Enumerable.Repeat((file, "Unused", string.Empty, 0x40000080u), 1)
: f.SubModUsage.Select(s => (file, s.Item2.ToString(), s.Item1.FullName, : f.SubModUsage.Select(s => (file, s.Item2.ToString(), s.Item1.GetFullName(),
_editor.Option! == s.Item1 && Mod!.HasOptions ? 0x40008000u : 0u)); _editor.Option! == s.Item1 && Mod!.HasOptions ? 0x40008000u : 0u));
}); });
@ -148,13 +147,13 @@ public partial class ModEditWindow
(string, int) GetMulti() (string, int) GetMulti()
{ {
var groups = registry.SubModUsage.GroupBy(s => s.Item1).ToArray(); var groups = registry.SubModUsage.GroupBy(s => s.Item1).ToArray();
return (string.Join("\n", groups.Select(g => g.Key.Name)), groups.Length); return (string.Join("\n", groups.Select(g => g.Key.GetName())), groups.Length);
} }
var (text, groupCount) = color switch var (text, groupCount) = color switch
{ {
ColorId.ConflictingMod => (string.Empty, 0), ColorId.ConflictingMod => (string.Empty, 0),
ColorId.NewMod => (registry.SubModUsage[0].Item1.Name, 1), ColorId.NewMod => (registry.SubModUsage[0].Item1.GetName(), 1),
ColorId.InheritedMod => GetMulti(), ColorId.InheritedMod => GetMulti(),
_ => (string.Empty, 0), _ => (string.Empty, 0),
}; };
@ -192,7 +191,7 @@ public partial class ModEditWindow
ImGuiUtil.RightAlign(rightText); ImGuiUtil.RightAlign(rightText);
} }
private void PrintGamePath(int i, int j, FileRegistry registry, SubMod subMod, Utf8GamePath gamePath) private void PrintGamePath(int i, int j, FileRegistry registry, IModDataContainer subMod, Utf8GamePath gamePath)
{ {
using var id = ImRaii.PushId(j); using var id = ImRaii.PushId(j);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
@ -228,7 +227,7 @@ public partial class ModEditWindow
} }
} }
private void PrintNewGamePath(int i, FileRegistry registry, SubMod subMod) private void PrintNewGamePath(int i, FileRegistry registry, IModDataContainer subMod)
{ {
var tmp = _fileIdx == i && _pathIdx == -1 ? _gamePathEdit : string.Empty; var tmp = _fileIdx == i && _pathIdx == -1 ? _gamePathEdit : string.Empty;
var pos = ImGui.GetCursorPosX() - ImGui.GetFrameHeight(); var pos = ImGui.GetCursorPosX() - ImGui.GetFrameHeight();
@ -301,9 +300,9 @@ public partial class ModEditWindow
tt = changes ? "Apply the current file setup to the currently selected option." : "No changes made."; tt = changes ? "Apply the current file setup to the currently selected option." : "No changes made.";
if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, !changes)) if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, !changes))
{ {
var failedFiles = _editor.FileEditor.Apply(_editor.Mod!, (SubMod)_editor.Option!); var failedFiles = _editor.FileEditor.Apply(_editor.Mod!, _editor.Option!);
if (failedFiles > 0) if (failedFiles > 0)
Penumbra.Log.Information($"Failed to apply {failedFiles} file redirections to {_editor.Option!.FullName}."); Penumbra.Log.Information($"Failed to apply {failedFiles} file redirections to {_editor.Option!.GetFullName()}.");
} }

View file

@ -45,7 +45,7 @@ public partial class ModEditWindow
var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option."; var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option.";
ImGui.NewLine(); ImGui.NewLine();
if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, setsEqual)) if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, setsEqual))
_editor.MetaEditor.Apply(_editor.Mod!, _editor.GroupIdx, _editor.OptionIdx); _editor.MetaEditor.Apply(_editor.Mod!, _editor.GroupIdx, _editor.DataIdx);
ImGui.SameLine(); ImGui.SameLine();
tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; tt = setsEqual ? "No changes staged." : "Revert all currently staged changes.";

View file

@ -85,7 +85,7 @@ public partial class ModEditWindow
{ {
// TODO: Is it worth trying to order results based on option priorities for cases where more than one match is found? // TODO: Is it worth trying to order results based on option priorities for cases where more than one match is found?
// NOTE: We're using case-insensitive comparisons, as option group paths in mods are stored in lower case, but the mod editor uses paths directly from the file system, which may be mixed case. // NOTE: We're using case-insensitive comparisons, as option group paths in mods are stored in lower case, but the mod editor uses paths directly from the file system, which may be mixed case.
return mod.AllSubMods return mod.AllDataContainers
.SelectMany(m => m.Files.Concat(m.FileSwaps)) .SelectMany(m => m.Files.Concat(m.FileSwaps))
.Where(kv => kv.Value.FullName.Equals(path, StringComparison.OrdinalIgnoreCase)) .Where(kv => kv.Value.FullName.Equals(path, StringComparison.OrdinalIgnoreCase))
.Select(kv => kv.Key) .Select(kv => kv.Key)
@ -103,7 +103,7 @@ public partial class ModEditWindow
return []; return [];
// Filter then prepend the current option to ensure it's chosen first. // Filter then prepend the current option to ensure it's chosen first.
return mod.AllSubMods return mod.AllDataContainers
.Where(subMod => subMod != option) .Where(subMod => subMod != option)
.Prepend(option) .Prepend(option)
.SelectMany(subMod => subMod.Manipulations) .SelectMany(subMod => subMod.Manipulations)

View file

@ -187,8 +187,8 @@ public partial class ModEditWindow
if (editor == null) if (editor == null)
return new QuickImportAction(owner._editor, FallbackOptionName, gamePath); return new QuickImportAction(owner._editor, FallbackOptionName, gamePath);
var subMod = editor.Option; var subMod = editor.Option!;
var optionName = subMod!.FullName; var optionName = subMod is IModOption o ? o.FullName : FallbackOptionName;
if (gamePath.IsEmpty || file == null || editor.FileEditor.Changes) if (gamePath.IsEmpty || file == null || editor.FileEditor.Changes)
return new QuickImportAction(editor, optionName, gamePath); return new QuickImportAction(editor, optionName, gamePath);
@ -199,7 +199,7 @@ public partial class ModEditWindow
if (mod == null) if (mod == null)
return new QuickImportAction(editor, optionName, gamePath); return new QuickImportAction(editor, optionName, gamePath);
var (preferredPath, subDirs) = GetPreferredPath(mod, subMod, owner._config.ReplaceNonAsciiOnImport); var (preferredPath, subDirs) = GetPreferredPath(mod, subMod as IModOption, owner._config.ReplaceNonAsciiOnImport);
var targetPath = new FullPath(Path.Combine(preferredPath.FullName, gamePath.ToString())).FullName; var targetPath = new FullPath(Path.Combine(preferredPath.FullName, gamePath.ToString())).FullName;
if (File.Exists(targetPath)) if (File.Exists(targetPath))
return new QuickImportAction(editor, optionName, gamePath); return new QuickImportAction(editor, optionName, gamePath);
@ -222,16 +222,16 @@ public partial class ModEditWindow
{ {
fileRegistry, fileRegistry,
}, _subDirs); }, _subDirs);
_editor.FileEditor.Apply(_editor.Mod!, (SubMod)_editor.Option!); _editor.FileEditor.Apply(_editor.Mod!, _editor.Option!);
return fileRegistry; return fileRegistry;
} }
private static (DirectoryInfo, int) GetPreferredPath(Mod mod, SubMod subMod, bool replaceNonAscii) private static (DirectoryInfo, int) GetPreferredPath(Mod mod, IModOption? subMod, bool replaceNonAscii)
{ {
var path = mod.ModPath; var path = mod.ModPath;
var subDirs = 0; var subDirs = 0;
if (subMod == mod.Default) if (subMod == null)
return (path, subDirs); return (path, subDirs);
var name = subMod.Name; var name = subMod.Name;

View file

@ -77,10 +77,10 @@ public partial class ModEditWindow : Window, IDisposable
_forceTextureStartPath = true; _forceTextureStartPath = true;
} }
public void ChangeOption(SubMod? subMod) public void ChangeOption(IModDataContainer? subMod)
{ {
var (groupIdx, optionIdx) = subMod?.GetIndices() ?? (-1, 0); var (groupIdx, dataIdx) = subMod?.GetDataIndices() ?? (-1, 0);
_editor.LoadOption(groupIdx, optionIdx); _editor.LoadOption(groupIdx, dataIdx);
} }
public void UpdateModels() public void UpdateModels()
@ -111,7 +111,7 @@ public partial class ModEditWindow : Window, IDisposable
}); });
var manipulations = 0; var manipulations = 0;
var subMods = 0; var subMods = 0;
var swaps = Mod!.AllSubMods.Sum(m => var swaps = Mod!.AllDataContainers.Sum(m =>
{ {
++subMods; ++subMods;
manipulations += m.Manipulations.Count; manipulations += m.Manipulations.Count;
@ -330,7 +330,7 @@ public partial class ModEditWindow : Window, IDisposable
else if (ImGuiUtil.DrawDisabledButton("Re-Duplicate and Normalize Mod", Vector2.Zero, tt, !_allowReduplicate && !modifier)) else if (ImGuiUtil.DrawDisabledButton("Re-Duplicate and Normalize Mod", Vector2.Zero, tt, !_allowReduplicate && !modifier))
{ {
_editor.ModNormalizer.Normalize(Mod!); _editor.ModNormalizer.Normalize(Mod!);
_editor.ModNormalizer.Worker.ContinueWith(_ => _editor.LoadMod(Mod!, _editor.GroupIdx, _editor.OptionIdx), TaskScheduler.Default); _editor.ModNormalizer.Worker.ContinueWith(_ => _editor.LoadMod(Mod!, _editor.GroupIdx, _editor.DataIdx), TaskScheduler.Default);
} }
if (!_editor.Duplicates.Worker.IsCompleted) if (!_editor.Duplicates.Worker.IsCompleted)
@ -405,7 +405,7 @@ public partial class ModEditWindow : Window, IDisposable
var width = new Vector2(ImGui.GetContentRegionAvail().X / 3, 0); var width = new Vector2(ImGui.GetContentRegionAvail().X / 3, 0);
var ret = false; var ret = false;
if (ImGuiUtil.DrawDisabledButton(defaultOption, width, "Switch to the default option for the mod.\nThis resets unsaved changes.", if (ImGuiUtil.DrawDisabledButton(defaultOption, width, "Switch to the default option for the mod.\nThis resets unsaved changes.",
_editor.Option!.IsDefault)) _editor.Option is DefaultSubMod))
{ {
_editor.LoadOption(-1, 0); _editor.LoadOption(-1, 0);
ret = true; ret = true;
@ -414,7 +414,7 @@ public partial class ModEditWindow : Window, IDisposable
ImGui.SameLine(); ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton("Refresh Data", width, "Refresh data for the current option.\nThis resets unsaved changes.", false)) if (ImGuiUtil.DrawDisabledButton("Refresh Data", width, "Refresh data for the current option.\nThis resets unsaved changes.", false))
{ {
_editor.LoadMod(_editor.Mod!, _editor.GroupIdx, _editor.OptionIdx); _editor.LoadMod(_editor.Mod!, _editor.GroupIdx, _editor.DataIdx);
ret = true; ret = true;
} }
@ -422,17 +422,17 @@ public partial class ModEditWindow : Window, IDisposable
ImGui.SetNextItemWidth(width.X); ImGui.SetNextItemWidth(width.X);
style.Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale); style.Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale);
using var color = ImRaii.PushColor(ImGuiCol.Border, ColorId.FolderLine.Value()); using var color = ImRaii.PushColor(ImGuiCol.Border, ColorId.FolderLine.Value());
using var combo = ImRaii.Combo("##optionSelector", _editor.Option.FullName); using var combo = ImRaii.Combo("##optionSelector", _editor.Option!.GetFullName());
if (!combo) if (!combo)
return ret; return ret;
foreach (var (option, idx) in Mod!.AllSubMods.WithIndex()) foreach (var (option, idx) in Mod!.AllDataContainers.WithIndex())
{ {
using var id = ImRaii.PushId(idx); using var id = ImRaii.PushId(idx);
if (ImGui.Selectable(option.FullName, option == _editor.Option)) if (ImGui.Selectable(option.GetFullName(), option == _editor.Option))
{ {
var (groupIdx, optionIdx) = option.GetIndices(); var (groupIdx, dataIdx) = option.GetDataIndices();
_editor.LoadOption(groupIdx, optionIdx); _editor.LoadOption(groupIdx, dataIdx);
ret = true; ret = true;
} }
} }
@ -455,7 +455,7 @@ public partial class ModEditWindow : Window, IDisposable
var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option."; var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option.";
ImGui.NewLine(); ImGui.NewLine();
if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, setsEqual)) if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, setsEqual))
_editor.SwapEditor.Apply(_editor.Mod!, _editor.GroupIdx, _editor.OptionIdx); _editor.SwapEditor.Apply(_editor.Mod!, _editor.GroupIdx, _editor.DataIdx);
ImGui.SameLine(); ImGui.SameLine();
tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; tt = setsEqual ? "No changes staged." : "Revert all currently staged changes.";
@ -569,7 +569,7 @@ public partial class ModEditWindow : Window, IDisposable
} }
if (Mod != null) if (Mod != null)
foreach (var option in Mod.AllSubMods) foreach (var option in Mod.AllDataContainers)
{ {
foreach (var path in option.Files.Keys) foreach (var path in option.Files.Keys)
{ {

View file

@ -50,8 +50,7 @@ public class ModMergeTab(ModMerger modMerger)
ImGui.SameLine(); ImGui.SameLine();
DrawCombo(size - ImGui.GetItemRectSize().X - ImGui.GetStyle().ItemSpacing.X); DrawCombo(size - ImGui.GetItemRectSize().X - ImGui.GetStyle().ItemSpacing.X);
var width = ImGui.GetItemRectSize(); using (ImRaii.Group())
using (var g = ImRaii.Group())
{ {
using var disabled = ImRaii.Disabled(modMerger.MergeFromMod.HasOptions); using var disabled = ImRaii.Disabled(modMerger.MergeFromMod.HasOptions);
var buttonWidth = (size - ImGui.GetStyle().ItemSpacing.X) / 2; var buttonWidth = (size - ImGui.GetStyle().ItemSpacing.X) / 2;
@ -124,13 +123,13 @@ public class ModMergeTab(ModMerger modMerger)
ImGui.Dummy(Vector2.One); ImGui.Dummy(Vector2.One);
var buttonSize = new Vector2((size - 2 * ImGui.GetStyle().ItemSpacing.X) / 3, 0); var buttonSize = new Vector2((size - 2 * ImGui.GetStyle().ItemSpacing.X) / 3, 0);
if (ImGui.Button("Select All", buttonSize)) if (ImGui.Button("Select All", buttonSize))
modMerger.SelectedOptions.UnionWith(modMerger.MergeFromMod!.AllSubMods); modMerger.SelectedOptions.UnionWith(modMerger.MergeFromMod!.AllDataContainers);
ImGui.SameLine(); ImGui.SameLine();
if (ImGui.Button("Unselect All", buttonSize)) if (ImGui.Button("Unselect All", buttonSize))
modMerger.SelectedOptions.Clear(); modMerger.SelectedOptions.Clear();
ImGui.SameLine(); ImGui.SameLine();
if (ImGui.Button("Invert Selection", buttonSize)) if (ImGui.Button("Invert Selection", buttonSize))
modMerger.SelectedOptions.SymmetricExceptWith(modMerger.MergeFromMod!.AllSubMods); modMerger.SelectedOptions.SymmetricExceptWith(modMerger.MergeFromMod!.AllDataContainers);
DrawOptionTable(size); DrawOptionTable(size);
} }
@ -144,7 +143,7 @@ public class ModMergeTab(ModMerger modMerger)
private void DrawOptionTable(float size) private void DrawOptionTable(float size)
{ {
var options = modMerger.MergeFromMod!.AllSubMods.ToList(); var options = modMerger.MergeFromMod!.AllDataContainers.ToList();
var height = modMerger.Warnings.Count == 0 && modMerger.Error == null var height = modMerger.Warnings.Count == 0 && modMerger.Error == null
? ImGui.GetContentRegionAvail().Y - 3 * ImGui.GetFrameHeightWithSpacing() ? ImGui.GetContentRegionAvail().Y - 3 * ImGui.GetFrameHeightWithSpacing()
: 8 * ImGui.GetFrameHeightWithSpacing(); : 8 * ImGui.GetFrameHeightWithSpacing();
@ -176,47 +175,41 @@ public class ModMergeTab(ModMerger modMerger)
if (ImGui.Checkbox("##check", ref selected)) if (ImGui.Checkbox("##check", ref selected))
Handle(option, selected); Handle(option, selected);
if (option.IsDefault) if (option.Group is not { } group)
{ {
ImGuiUtil.DrawTableColumn(option.FullName); ImGuiUtil.DrawTableColumn(option.GetFullName());
ImGui.TableNextColumn(); ImGui.TableNextColumn();
} }
else else
{ {
ImGuiUtil.DrawTableColumn(option.Name); ImGuiUtil.DrawTableColumn(option.GetName());
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"))
// ReSharper disable once PossibleMultipleEnumeration // ReSharper disable once PossibleMultipleEnumeration
foreach (var opt in optionEnumerator) foreach (var opt in group.DataContainers)
Handle(opt, true); Handle(opt, true);
if (ImGui.MenuItem("Unselect All")) if (ImGui.MenuItem("Unselect All"))
// ReSharper disable once PossibleMultipleEnumeration // ReSharper disable once PossibleMultipleEnumeration
foreach (var opt in optionEnumerator) foreach (var opt in group.DataContainers)
Handle(opt, false); Handle(opt, false);
ImGui.EndPopup(); ImGui.EndPopup();
} }
} }
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGuiUtil.RightAlign(option.FileData.Count.ToString(), 3 * ImGuiHelpers.GlobalScale); ImGuiUtil.RightAlign(option.Files.Count.ToString(), 3 * ImGuiHelpers.GlobalScale);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGuiUtil.RightAlign(option.FileSwapData.Count.ToString(), 3 * ImGuiHelpers.GlobalScale); ImGuiUtil.RightAlign(option.FileSwaps.Count.ToString(), 3 * ImGuiHelpers.GlobalScale);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGuiUtil.RightAlign(option.Manipulations.Count.ToString(), 3 * ImGuiHelpers.GlobalScale); ImGuiUtil.RightAlign(option.Manipulations.Count.ToString(), 3 * ImGuiHelpers.GlobalScale);
continue; continue;
void Handle(SubMod option2, bool selected2) void Handle(IModDataContainer option2, bool selected2)
{ {
if (selected2) if (selected2)
modMerger.SelectedOptions.Add(option2); modMerger.SelectedOptions.Add(option2);

View file

@ -486,7 +486,7 @@ public class ModPanelEditTab(
EditOption(panel, single, groupIdx, optionIdx); EditOption(panel, single, groupIdx, optionIdx);
break; break;
case MultiModGroup multi: case MultiModGroup multi:
for (var optionIdx = 0; optionIdx < multi.PrioritizedOptions.Count; ++optionIdx) for (var optionIdx = 0; optionIdx < multi.OptionData.Count; ++optionIdx)
EditOption(panel, multi, groupIdx, optionIdx); EditOption(panel, multi, groupIdx, optionIdx);
break; break;
} }
@ -542,7 +542,7 @@ public class ModPanelEditTab(
if (group is not MultiModGroup multi) if (group is not MultiModGroup multi)
return; return;
if (Input.Priority("##Priority", groupIdx, optionIdx, multi.PrioritizedOptions[optionIdx].Priority, out var priority, if (Input.Priority("##Priority", groupIdx, optionIdx, multi.OptionData[optionIdx].Priority, out var priority,
50 * UiHelpers.Scale)) 50 * UiHelpers.Scale))
panel._modManager.OptionEditor.ChangeOptionPriority(panel._mod, groupIdx, optionIdx, priority); panel._modManager.OptionEditor.ChangeOptionPriority(panel._mod, groupIdx, optionIdx, priority);
@ -557,7 +557,7 @@ public class ModPanelEditTab(
var count = group switch var count = group switch
{ {
SingleModGroup single => single.OptionData.Count, SingleModGroup single => single.OptionData.Count,
MultiModGroup multi => multi.PrioritizedOptions.Count, MultiModGroup multi => multi.OptionData.Count,
_ => throw new Exception($"Dragging options to an option group of type {group.GetType()} is not supported."), _ => throw new Exception($"Dragging options to an option group of type {group.GetType()} is not supported."),
}; };
ImGui.TableNextColumn(); ImGui.TableNextColumn();
@ -591,6 +591,9 @@ public class ModPanelEditTab(
// Handle drag and drop to move options inside a group or into another group. // 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(IModGroup group, int groupIdx, int optionIdx)
{ {
if (group is not ITexToolsGroup)
return;
using var source = ImRaii.DragDropSource(); using var source = ImRaii.DragDropSource();
if (!source) if (!source)
return; return;
@ -606,6 +609,9 @@ public class ModPanelEditTab(
private static void Target(ModPanelEditTab panel, IModGroup group, int groupIdx, int optionIdx) private static void Target(ModPanelEditTab panel, IModGroup group, int groupIdx, int optionIdx)
{ {
if (group is not ITexToolsGroup)
return;
using var target = ImRaii.DragDropTarget(); using var target = ImRaii.DragDropTarget();
if (!target.Success || !ImGuiUtil.IsDropping(DragDropLabel)) if (!target.Success || !ImGuiUtil.IsDropping(DragDropLabel))
return; return;
@ -624,22 +630,12 @@ 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 switch var currentCount = group.DataContainers.Count;
{ var option = ((ITexToolsGroup) sourceGroup).OptionData[_dragDropOptionIdx];
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."),
};
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(() =>
{ {
panel._modManager.OptionEditor.DeleteOption(panel._mod, sourceGroupIdx, sourceOption); panel._modManager.OptionEditor.DeleteOption(panel._mod, sourceGroupIdx, sourceOption);
panel._modManager.OptionEditor.AddOption(panel._mod, groupIdx, option, priority); panel._modManager.OptionEditor.AddOption(panel._mod, groupIdx, option);
panel._modManager.OptionEditor.MoveOption(panel._mod, groupIdx, currentCount, optionIdx); panel._modManager.OptionEditor.MoveOption(panel._mod, groupIdx, currentCount, optionIdx);
}); });
} }

View file

@ -114,7 +114,7 @@ public class ModPanelTabBar
if (ImGui.TabItemButton("Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip)) if (ImGui.TabItemButton("Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip))
{ {
_modEditWindow.ChangeMod(mod); _modEditWindow.ChangeMod(mod);
_modEditWindow.ChangeOption((SubMod)mod.Default); _modEditWindow.ChangeOption(mod.Default);
_modEditWindow.IsOpen = true; _modEditWindow.IsOpen = true;
} }