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)
{
var optionIdx = multi.PrioritizedOptions.IndexOf(o => o.Mod.Name == name);
var optionIdx = multi.OptionData.IndexOf(o => o.Mod.Name == name);
if (optionIdx < 0)
return ApiHelpers.Return(PenumbraApiEc.OptionMissing, args);

View file

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

View file

@ -50,17 +50,15 @@ public unsafe class MetaFileManager
TexToolsMeta.WriteTexToolsMeta(this, mod.Default.Manipulations, mod.ModPath);
foreach (var group in mod.Groups)
{
if (group is not ITexToolsGroup texToolsGroup)
continue;
var dir = ModCreator.NewOptionDirectory(mod.ModPath, group.Name, Config.ReplaceNonAsciiOnImport);
if (!dir.Exists)
dir.Create();
var optionEnumerator = group switch
{
SingleModGroup single => single.OptionData,
MultiModGroup multi => multi.PrioritizedOptions.Select(o => o.Mod),
_ => [],
};
foreach (var option in optionEnumerator)
foreach (var option in texToolsGroup.OptionData)
{
var optionDir = ModCreator.NewOptionDirectory(dir, option.Name, Config.ReplaceNonAsciiOnImport);
if (!optionDir.Exists)
@ -99,7 +97,7 @@ public unsafe class MetaFileManager
return;
ResidentResources.Reload();
if (collection?._cache == null)
if (collection._cache == null)
CharacterUtility.ResetAll();
else
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);
}
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)
return;
@ -72,7 +72,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co
return;
void HandleSubMod(SubMod subMod, int groupIdx, int optionIdx)
void HandleSubMod(IModDataContainer subMod, int groupIdx, int optionIdx)
{
var changes = false;
var dict = subMod.Files.ToDictionary(kvp => kvp.Key,
@ -86,7 +86,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co
}
else
{
subMod.FileData = dict;
subMod.Files = dict;
saveService.ImmediateSaveSync(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
}
}

View file

@ -5,12 +5,12 @@ namespace Penumbra.Mods.Editor;
public class FileRegistry : IEquatable<FileRegistry>
{
public readonly List<(SubMod, Utf8GamePath)> SubModUsage = [];
public FullPath File { get; private init; }
public Utf8RelPath RelPath { get; private init; }
public long FileSize { get; private init; }
public int CurrentUsage;
public bool IsOnPlayer;
public readonly List<(IModDataContainer, Utf8GamePath)> SubModUsage = [];
public FullPath File { get; private init; }
public Utf8RelPath RelPath { get; private init; }
public long FileSize { get; private init; }
public int CurrentUsage;
public bool IsOnPlayer;
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.Compression;
using Penumbra.Mods.Subclasses;
@ -25,20 +24,20 @@ public class ModEditor(
public readonly MdlMaterialEditor MdlMaterialEditor = mdlMaterialEditor;
public readonly FileCompactor Compactor = compactor;
public Mod? Mod { get; private set; }
public int GroupIdx { get; private set; }
public int OptionIdx { get; private set; }
public Mod? Mod { get; private set; }
public int GroupIdx { get; private set; }
public int DataIdx { get; private set; }
public IModGroup? Group { get; private set; }
public SubMod? Option { get; private set; }
public IModGroup? Group { get; private set; }
public IModDataContainer? Option { get; private set; }
public void LoadMod(Mod mod)
=> LoadMod(mod, -1, 0);
public void LoadMod(Mod mod, int groupIdx, int optionIdx)
public void LoadMod(Mod mod, int groupIdx, int dataIdx)
{
Mod = mod;
LoadOption(groupIdx, optionIdx, true);
LoadOption(groupIdx, dataIdx, true);
Files.UpdateAll(mod, Option!);
SwapEditor.Revert(Option!);
MetaEditor.Load(Mod!, Option!);
@ -46,9 +45,9 @@ public class ModEditor(
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!);
Files.UpdatePaths(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>
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 (groupIdx == -1 && optionIdx == 0)
if (groupIdx == -1 && dataIdx == 0)
{
Group = null;
Option = Mod.Default;
GroupIdx = groupIdx;
OptionIdx = optionIdx;
Group = null;
Option = Mod.Default;
GroupIdx = groupIdx;
DataIdx = dataIdx;
return;
}
if (groupIdx >= 0)
{
Group = Mod.Groups[groupIdx];
switch(Group)
if (dataIdx >= 0 && dataIdx < Group.DataContainers.Count)
{
case SingleModGroup single when optionIdx >= 0 && optionIdx < single.OptionData.Count:
Option = single.OptionData[optionIdx];
GroupIdx = groupIdx;
OptionIdx = optionIdx;
return;
case MultiModGroup multi when optionIdx >= 0 && optionIdx < multi.PrioritizedOptions.Count:
Option = multi.PrioritizedOptions[optionIdx].Mod;
GroupIdx = groupIdx;
OptionIdx = optionIdx;
return;
Option = Group.DataContainers[dataIdx];
GroupIdx = groupIdx;
DataIdx = dataIdx;
return;
}
}
}
Group = null;
Option = Mod?.Default;
GroupIdx = -1;
OptionIdx = 0;
Group = null;
Option = Mod?.Default;
GroupIdx = -1;
DataIdx = 0;
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()
@ -111,7 +104,7 @@ public class ModEditor(
=> Clear();
/// <summary> Apply a option action to all available option in a mod, including the default option. </summary>
public static void ApplyToAllOptions(Mod mod, Action<SubMod, int, int> action)
public static void ApplyToAllOptions(Mod mod, Action<IModDataContainer, int, int> action)
{
action(mod.Default, -1, 0);
foreach (var (group, groupIdx) in mod.Groups.WithIndex())
@ -123,8 +116,8 @@ public class ModEditor(
action(single.OptionData[optionIdx], groupIdx, optionIdx);
break;
case MultiModGroup multi:
for (var optionIdx = 0; optionIdx < multi.PrioritizedOptions.Count; ++optionIdx)
action(multi.PrioritizedOptions[optionIdx].Mod, groupIdx, optionIdx);
for (var optionIdx = 0; optionIdx < multi.OptionData.Count; ++optionIdx)
action(multi.OptionData[optionIdx], groupIdx, optionIdx);
break;
}
}

View file

@ -38,13 +38,13 @@ public class ModFileCollection : IDisposable
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());
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());
public void Clear()
@ -59,7 +59,7 @@ public class ModFileCollection : IDisposable
public void ClearMissingFiles()
=> _missing.Clear();
public void RemoveUsedPath(SubMod option, FileRegistry? file, Utf8GamePath gamePath)
public void RemoveUsedPath(IModDataContainer option, FileRegistry? file, Utf8GamePath gamePath)
{
_usedPaths.Remove(gamePath);
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);
public void AddUsedPath(SubMod option, FileRegistry? file, Utf8GamePath gamePath)
public void AddUsedPath(IModDataContainer option, FileRegistry? file, Utf8GamePath gamePath)
{
_usedPaths.Add(gamePath);
if (file == null)
@ -82,7 +82,7 @@ public class ModFileCollection : IDisposable
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);
public void ChangeUsedPath(FileRegistry file, int pathIdx, Utf8GamePath gamePath)
@ -154,14 +154,14 @@ public class ModFileCollection : IDisposable
_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();
ClearPaths(clearRegistries, tok);
tok.ThrowIfCancellationRequested();
foreach (var subMod in mod.AllSubMods)
foreach (var subMod in mod.AllDataContainers)
{
foreach (var (gamePath, file) in subMod.Files)
{

View file

@ -14,7 +14,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu
Changes = false;
}
public int Apply(Mod mod, SubMod option)
public int Apply(Mod mod, IModDataContainer option)
{
var dict = new Dictionary<Utf8GamePath, FullPath>();
var num = 0;
@ -24,23 +24,23 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager, Commu
num += dict.TryAdd(path.Item2, file.File) ? 0 : 1;
}
var (groupIdx, optionIdx) = option.GetIndices();
modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, dict);
var (groupIdx, dataIdx) = option.GetDataIndices();
modManager.OptionEditor.OptionSetFiles(mod, groupIdx, dataIdx, dict);
files.UpdatePaths(mod, option);
Changes = false;
return num;
}
public void Revert(Mod mod, SubMod option)
public void Revert(Mod mod, IModDataContainer option)
{
files.UpdateAll(mod, option);
Changes = false;
}
/// <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))
.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 pathIdx is equal to the total number of paths, path will be added, otherwise replaced.
/// </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)
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,
/// and add them to the given option.
/// </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;
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>
public void RemovePathsFromSelected(SubMod option, IEnumerable<FileRegistry> files1)
public void RemovePathsFromSelected(IModDataContainer option, IEnumerable<FileRegistry> 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>
public void DeleteFiles(Mod mod, SubMod option, IEnumerable<FileRegistry> files1)
public void DeleteFiles(Mod mod, IModDataContainer option, IEnumerable<FileRegistry> files1)
{
var deletions = 0;
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))
return true;

View file

@ -1,6 +1,5 @@
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Utility;
using ImGuizmoNET;
using OtterGui;
using OtterGui.Classes;
using Penumbra.Api.Enums;
@ -33,9 +32,9 @@ public class ModMerger : IDisposable
private readonly Dictionary<string, string> _fileToFile = [];
private readonly HashSet<string> _createdDirectories = [];
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 Exception? Error { get; private set; }
@ -94,7 +93,7 @@ public class ModMerger : IDisposable
private void MergeWithOptions()
{
MergeIntoOption(Enumerable.Repeat(MergeFromMod!.Default, 1), MergeToMod!.Default, false);
MergeIntoOption([MergeFromMod!.Default], MergeToMod!.Default, false);
foreach (var originalGroup in MergeFromMod!.Groups)
{
@ -105,20 +104,13 @@ public class ModMerger : IDisposable
((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}.");
var optionEnumerator = group switch
foreach (var originalOption in group.DataContainers)
{
SingleModGroup single => single.OptionData,
MultiModGroup multi => multi.PrioritizedOptions.Select(o => o.Mod),
_ => [],
};
foreach (var originalOption in optionEnumerator)
{
var (option, _, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, originalOption.Name);
var (option, _, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, originalOption.GetName());
if (optionCreated)
{
_createdOptions.Add(option);
MergeIntoOption(Enumerable.Repeat(originalOption, 1), option, false);
_createdOptions.Add((IModDataOption)option);
MergeIntoOption([originalOption], (IModDataOption)option, false);
}
else
{
@ -136,7 +128,7 @@ public class ModMerger : IDisposable
if (groupName.Length == 0 && optionName.Length == 0)
{
CopyFiles(MergeToMod!.ModPath);
MergeIntoOption(MergeFromMod!.AllSubMods.Reverse(), MergeToMod!.Default, true);
MergeIntoOption(MergeFromMod!.AllDataContainers.Reverse(), MergeToMod!.Default, true);
}
else if (groupName.Length * optionName.Length == 0)
{
@ -148,7 +140,7 @@ public class ModMerger : IDisposable
_createdGroups.Add(groupIdx);
var (option, _, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, optionName, SaveType.None);
if (optionCreated)
_createdOptions.Add(option);
_createdOptions.Add((IModDataOption)option);
var dir = ModCreator.NewOptionDirectory(MergeToMod!.ModPath, groupName, _config.ReplaceNonAsciiOnImport);
if (!dir.Exists)
_createdDirectories.Add(dir.FullName);
@ -156,14 +148,14 @@ public class ModMerger : IDisposable
if (!dir.Exists)
_createdDirectories.Add(dir.FullName);
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 swaps = option.FileSwapData.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
var manips = option.ManipulationData.ToHashSet();
var redirections = option.Files.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
var swaps = option.FileSwaps.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
var manips = option.Manipulations.ToHashSet();
foreach (var originalOption in mergeOptions)
{
@ -171,31 +163,31 @@ public class ModMerger : IDisposable
{
if (!manips.Add(manip))
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)
{
if (!swaps.TryAdd(swapA, swapB))
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)
{
if (!GetFullPath(path, out var newFile))
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))
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();
_editor.OptionSetFiles(MergeToMod!, groupIdx, optionIdx, redirections, SaveType.None);
_editor.OptionSetFileSwaps(MergeToMod!, groupIdx, optionIdx, swaps, SaveType.None);
_editor.OptionSetManipulations(MergeToMod!, groupIdx, optionIdx, manips, SaveType.ImmediateSync);
var (groupIdx, dataIdx) = option.GetDataIndices();
_editor.OptionSetFiles(MergeToMod!, groupIdx, dataIdx, redirections, SaveType.None);
_editor.OptionSetFileSwaps(MergeToMod!, groupIdx, dataIdx, swaps, SaveType.None);
_editor.OptionSetManipulations(MergeToMod!, groupIdx, dataIdx, manips, SaveType.ImmediateSync);
return;
bool GetFullPath(FullPath input, out FullPath ret)
@ -270,30 +262,29 @@ public class ModMerger : IDisposable
{
var files = CopySubModFiles(mods[0], dir);
_editor.OptionSetFiles(result, -1, 0, files);
_editor.OptionSetFileSwaps(result, -1, 0, mods[0].FileSwapData);
_editor.OptionSetManipulations(result, -1, 0, mods[0].ManipulationData);
_editor.OptionSetFileSwaps(result, -1, 0, mods[0].FileSwaps);
_editor.OptionSetManipulations(result, -1, 0, mods[0].Manipulations);
}
else
{
foreach (var originalOption in mods)
{
if (originalOption.IsDefault)
if (originalOption.Group is not {} originalGroup)
{
var files = CopySubModFiles(mods[0], dir);
_editor.OptionSetFiles(result, -1, 0, files);
_editor.OptionSetFileSwaps(result, -1, 0, mods[0].FileSwapData);
_editor.OptionSetManipulations(result, -1, 0, mods[0].ManipulationData);
_editor.OptionSetFileSwaps(result, -1, 0, mods[0].FileSwaps);
_editor.OptionSetManipulations(result, -1, 0, mods[0].Manipulations);
}
else
{
var originalGroup = originalOption.Group;
var (group, groupIdx, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name);
var (option, optionIdx, _) = _editor.FindOrAddOption(result, groupIdx, originalOption.Name);
var (group, groupIdx, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name);
var (option, optionIdx, _) = _editor.FindOrAddOption(result, groupIdx, originalOption.GetName());
var folder = Path.Combine(dir.FullName, group.Name, option.Name);
var files = CopySubModFiles(originalOption, new DirectoryInfo(folder));
_editor.OptionSetFiles(result, groupIdx, optionIdx, files);
_editor.OptionSetFileSwaps(result, groupIdx, optionIdx, originalOption.FileSwapData);
_editor.OptionSetManipulations(result, groupIdx, optionIdx, originalOption.ManipulationData);
_editor.OptionSetFileSwaps(result, groupIdx, optionIdx, originalOption.FileSwaps);
_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;
foreach (var (path, file) in option.FileData)
foreach (var (path, file) in option.Files)
{
var target = Path.GetRelativePath(parentPath, file.FullName);
target = Path.Combine(newMod.FullName, target);
@ -348,7 +339,7 @@ public class ModMerger : IDisposable
{
foreach (var option in _createdOptions)
{
var (groupIdx, optionIdx) = option.GetIndices();
var (groupIdx, optionIdx) = option.GetOptionIndices();
_editor.DeleteOption(MergeToMod!, groupIdx, optionIdx);
Penumbra.Log.Verbose($"[Merger] Removed option {option.FullName}.");
}

View file

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

View file

@ -1,3 +1,4 @@
using System;
using Dalamud.Interface.Internal.Notifications;
using OtterGui;
using OtterGui.Classes;
@ -168,27 +169,11 @@ public class ModNormalizer(ModManager _modManager, Configuration _config)
foreach (var (group, groupIdx) in Mod.Groups.WithIndex())
{
var groupDir = ModCreator.CreateModFolder(directory, group.Name, _config.ReplaceNonAsciiOnImport, true);
switch (group)
{
case SingleModGroup single:
_redirections[groupIdx + 1].EnsureCapacity(single.OptionData.Count);
for (var i = _redirections[groupIdx + 1].Count; i < single.OptionData.Count; ++i)
_redirections[groupIdx + 1].Add([]);
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;
}
_redirections[groupIdx + 1].EnsureCapacity(group.DataContainers.Count);
for (var i = _redirections[groupIdx + 1].Count; i < group.DataContainers.Count; ++i)
_redirections[groupIdx + 1].Add([]);
foreach (var (data, dataIdx) in group.DataContainers.WithIndex())
HandleSubMod(groupDir, data, _redirections[groupIdx + 1][dataIdx]);
}
return true;
@ -200,13 +185,14 @@ public class ModNormalizer(ModManager _modManager, Configuration _config)
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.EnsureCapacity(option.FileData.Count);
foreach (var (gamePath, fullPath) in option.FileData)
newDict.EnsureCapacity(option.Files.Count);
foreach (var (gamePath, fullPath) in option.Files)
{
var relPath = new Utf8RelPath(gamePath).ToString();
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]);
break;
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]);
break;
}

View file

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

View file

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

View file

@ -71,14 +71,14 @@ public static partial class ModMigration
foreach (var unusedFile in mod.FindUnusedFiles().Where(f => !seenMetaFiles.Contains(f)))
{
if (unusedFile.ToGamePath(mod.ModPath, out var gamePath)
&& !mod.Default.FileData.TryAdd(gamePath, unusedFile))
Penumbra.Log.Error($"Could not add {gamePath} because it already points to {mod.Default.FileData[gamePath]}.");
&& !mod.Default.Files.TryAdd(gamePath, unusedFile))
Penumbra.Log.Error($"Could not add {gamePath} because it already points to {mod.Default.Files[gamePath]}.");
}
mod.Default.FileSwapData.Clear();
mod.Default.FileSwapData.EnsureCapacity(swaps.Count);
mod.Default.FileSwaps.Clear();
mod.Default.FileSwaps.EnsureCapacity(swaps.Count);
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);
foreach (var (_, index) in mod.Groups.WithIndex())
@ -134,7 +134,7 @@ public static partial class ModMigration
};
mod.Groups.Add(newMultiGroup);
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;
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)
{
var fullPath = new FullPath(basePath, relPath);
foreach (var gamePath in gamePaths)
mod.FileData.TryAdd(gamePath, fullPath);
mod.Files.TryAdd(gamePath, fullPath);
if (fullPath.Extension is ".meta" or ".rgsp")
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);
creator.IncorporateMetaChanges(subMod, mod.ModPath, false);
return subMod;

View file

@ -1,4 +1,3 @@
using System.Security.AccessControl;
using Dalamud.Interface.Internal.Notifications;
using OtterGui;
using OtterGui.Classes;
@ -179,10 +178,10 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS
switch (mod.Groups[groupIdx])
{
case MultiModGroup multi:
if (multi.PrioritizedOptions[optionIdx].Priority == newPriority)
if (multi.OptionData[optionIdx].Priority == newPriority)
return;
multi.PrioritizedOptions[optionIdx] = (multi.PrioritizedOptions[optionIdx].Mod, newPriority);
multi.OptionData[optionIdx].Priority = newPriority;
saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1);
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>
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];
switch (group)
{
case SingleModGroup single:
{
var idx = single.OptionData.IndexOf(o => o.Name == newName);
if (idx >= 0)
return (single.OptionData[idx], idx, false);
var idx = group.Options.IndexOf(o => o.Name == newName);
if (idx >= 0)
return (group.Options[idx], idx, false);
idx = single.AddOption(mod, newName);
if (idx < 0)
throw new Exception($"Could not create new option with name {newName} in {group.Name}.");
idx = group.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 (single.OptionData[^1], single.OptionData.Count - 1, true);
}
case MultiModGroup multi:
{
var idx = multi.PrioritizedOptions.IndexOf(o => o.Mod.Name == newName);
if (idx >= 0)
return (multi.PrioritizedOptions[idx].Mod, idx, false);
idx = multi.AddOption(mod, newName);
if (idx < 0)
throw new Exception($"Could not create new option with name {newName} in {group.Name}.");
saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1);
return (multi.PrioritizedOptions[^1].Mod, multi.PrioritizedOptions.Count - 1, true);
}
}
throw new Exception($"{nameof(FindOrAddOption)} is not supported for mod groups of type {group.GetType()}.");
saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, idx, -1);
return (group.Options[idx], idx, true);
}
/// <summary> Add an existing option to a given group with default priority. </summary>
public void AddOption(Mod mod, int groupIdx, SubMod 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)
/// <summary> Add an existing option to a given group. </summary>
public void AddOption(Mod mod, int groupIdx, IModOption option)
{
var group = mod.Groups[groupIdx];
int idx;
switch (group)
{
case MultiModGroup { PrioritizedOptions.Count: >= IModGroup.MaxMultiOptions }:
case MultiModGroup { OptionData.Count: >= IModGroup.MaxMultiOptions }:
Penumbra.Log.Error(
$"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, "
+ $"since only up to {IModGroup.MaxMultiOptions} options are supported in one group.");
return;
case SingleModGroup s:
{
idx = s.OptionData.Count;
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;
}
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;
default:
return;
}
default: return;
}
saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
@ -295,7 +286,7 @@ public class ModOptionEditor(CommunicatorService communicator, SaveService saveS
break;
case MultiModGroup m:
m.PrioritizedOptions.RemoveAt(optionIdx);
m.OptionData.RemoveAt(optionIdx);
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>
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)
{
var subMod = GetSubMod(mod, groupIdx, optionIdx);
var subMod = GetSubMod(mod, groupIdx, dataContainerIdx);
if (subMod.Manipulations.Count == manipulations.Count
&& subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m)))
return;
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
subMod.ManipulationData.SetTo(manipulations);
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, dataContainerIdx, -1);
subMod.Manipulations.SetTo(manipulations);
saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1);
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, dataContainerIdx, -1);
}
/// <summary> Set the file redirections for a given option. Replaces existing redirections. </summary>
public void OptionSetFiles(Mod mod, int groupIdx, int optionIdx, IReadOnlyDictionary<Utf8GamePath, FullPath> replacements,
public void OptionSetFiles(Mod mod, int groupIdx, int dataContainerIdx, IReadOnlyDictionary<Utf8GamePath, FullPath> replacements,
SaveType saveType = SaveType.Queue)
{
var subMod = GetSubMod(mod, groupIdx, optionIdx);
if (subMod.FileData.SetEquals(replacements))
var subMod = GetSubMod(mod, groupIdx, dataContainerIdx);
if (subMod.Files.SetEquals(replacements))
return;
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
subMod.FileData.SetTo(replacements);
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, dataContainerIdx, -1);
subMod.Files.SetTo(replacements);
saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, 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>
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 oldCount = subMod.FileData.Count;
subMod.FileData.AddFrom(additions);
if (oldCount != subMod.FileData.Count)
var subMod = GetSubMod(mod, groupIdx, dataContainerIdx);
var oldCount = subMod.Files.Count;
subMod.Files.AddFrom(additions);
if (oldCount != subMod.Files.Count)
{
saveService.QueueSave(new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, optionIdx, -1);
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, dataContainerIdx, -1);
}
}
/// <summary> Set the file swaps for a given option. Replaces existing swaps. </summary>
public void OptionSetFileSwaps(Mod mod, int groupIdx, int optionIdx, IReadOnlyDictionary<Utf8GamePath, FullPath> swaps,
public void OptionSetFileSwaps(Mod mod, int groupIdx, int dataContainerIdx, IReadOnlyDictionary<Utf8GamePath, FullPath> swaps,
SaveType saveType = SaveType.Queue)
{
var subMod = GetSubMod(mod, groupIdx, optionIdx);
if (subMod.FileSwapData.SetEquals(swaps))
var subMod = GetSubMod(mod, groupIdx, dataContainerIdx);
if (subMod.FileSwaps.SetEquals(swaps))
return;
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
subMod.FileSwapData.SetTo(swaps);
communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, dataContainerIdx, -1);
subMod.FileSwaps.SetTo(swaps);
saveService.Save(saveType, new ModSaveGroup(mod, groupIdx, config.ReplaceNonAsciiOnImport));
communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, 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>
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.Groups[groupIdx] switch
{
SingleModGroup s => s.OptionData[optionIdx],
MultiModGroup m => m.PrioritizedOptions[optionIdx].Mod,
_ => throw new InvalidOperationException(),
};
return mod.Groups[groupIdx].DataContainers[dataContainerIdx];
}
}

View file

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

View file

@ -112,10 +112,8 @@ public partial class ModCreator(
var defaultFile = _saveService.FileNames.OptionGroupFile(mod, -1, Config.ReplaceNonAsciiOnImport);
try
{
if (!File.Exists(defaultFile))
mod.Default.Load(mod.ModPath, new JObject(), out _);
else
mod.Default.Load(mod.ModPath, JObject.Parse(File.ReadAllText(defaultFile)), out _);
var jObject = File.Exists(defaultFile) ? JObject.Parse(File.ReadAllText(defaultFile)) : new JObject();
IModDataContainer.Load(jObject, mod.Default, mod.ModPath);
}
catch (Exception e)
{
@ -154,7 +152,7 @@ public partial class ModCreator(
{
var changes = false;
List<string> deleteList = new();
foreach (var subMod in mod.AllSubMods)
foreach (var subMod in mod.AllDataContainers)
{
var (localChanges, localDeleteList) = IncorporateMetaChanges(subMod, mod.ModPath, false);
changes |= localChanges;
@ -162,7 +160,7 @@ public partial class ModCreator(
deleteList.AddRange(localDeleteList);
}
SubMod.DeleteDeleteList(deleteList, delete);
IModDataContainer.DeleteDeleteList(deleteList, delete);
if (!changes)
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 delete is true, the files are deleted afterwards.
/// </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 oldSize = option.ManipulationData.Count;
var oldSize = option.Manipulations.Count;
var deleteString = delete ? "with deletion." : "without deletion.";
foreach (var (key, file) in option.Files.ToList())
{
@ -189,7 +187,7 @@ public partial class ModCreator(
{
if (ext1 == ".meta" || ext2 == ".meta")
{
option.FileData.Remove(key);
option.Files.Remove(key);
if (!file.Exists)
continue;
@ -198,11 +196,11 @@ public partial class ModCreator(
Penumbra.Log.Verbose(
$"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}");
deleteList.Add(file.FullName);
option.ManipulationData.UnionWith(meta.MetaManipulations);
option.Manipulations.UnionWith(meta.MetaManipulations);
}
else if (ext1 == ".rgsp" || ext2 == ".rgsp")
{
option.FileData.Remove(key);
option.Files.Remove(key);
if (!file.Exists)
continue;
@ -212,7 +210,7 @@ public partial class ModCreator(
$"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}");
deleteList.Add(file.FullName);
option.ManipulationData.UnionWith(rgsp.MetaManipulations);
option.Manipulations.UnionWith(rgsp.MetaManipulations);
}
}
catch (Exception e)
@ -221,8 +219,8 @@ public partial class ModCreator(
}
}
SubMod.DeleteDeleteList(deleteList, delete);
return (oldSize < option.ManipulationData.Count, deleteList);
IModDataContainer.DeleteDeleteList(deleteList, delete);
return (oldSize < option.Manipulations.Count, deleteList);
}
/// <summary>
@ -238,7 +236,7 @@ public partial class ModCreator(
/// <summary> Create a file for an option group from given data. </summary>
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)
{
@ -248,7 +246,7 @@ public partial class ModCreator(
group.Description = desc;
group.Priority = priority;
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));
break;
}
@ -258,7 +256,7 @@ public partial class ModCreator(
group.Description = desc;
group.Priority = priority;
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));
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>
public SubMod CreateSubMod(DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option)
public MultiSubMod CreateSubMod(DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option, ModPriority priority)
{
var list = optionFolder.EnumerateNonHiddenFiles()
.Select(f => (Utf8GamePath.FromFile(f, optionFolder, out var gamePath, true), gamePath, new FullPath(f)))
.Where(t => t.Item1);
var mod = SubMod.CreateForSaving(option.Name);
mod.Description = option.Description;
var mod = MultiSubMod.CreateForSaving(option.Name, option.Description, priority);
foreach (var (_, gamePath, file) in list)
mod.FileData.TryAdd(gamePath, file);
mod.Files.TryAdd(gamePath, file);
IncorporateMetaChanges(mod, baseFolder, true);
return mod;
@ -292,7 +289,7 @@ public partial class ModCreator(
foreach (var file in mod.FindUnusedFiles())
{
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);

View file

@ -1,12 +1,16 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Editor;
using Penumbra.String.Classes;
namespace Penumbra.Mods.Subclasses;
public interface IModDataContainer
{
public IMod Mod { get; }
public IModGroup? Group { get; }
public Dictionary<Utf8GamePath, FullPath> Files { get; set; }
public Dictionary<Utf8GamePath, FullPath> FileSwaps { get; set; }
public HashSet<MetaManipulation> Manipulations { get; set; }
@ -21,6 +25,32 @@ public interface IModDataContainer
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)
{
data.Files.Clear();
@ -77,4 +107,22 @@ public interface IModDataContainer
serializer.Serialize(j, data.Manipulations);
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 Penumbra.Api.Enums;
using Penumbra.Meta.Manipulations;
@ -6,6 +7,11 @@ using Penumbra.String.Classes;
namespace Penumbra.Mods.Subclasses;
public interface ITexToolsGroup
{
public IReadOnlyList<IModDataOption> OptionData { get; }
}
public interface IModGroup
{
public const int MaxMultiOptions = 63;
@ -19,28 +25,89 @@ public interface IModGroup
public FullPath? FindBestMatch(Utf8GamePath gamePath);
public int AddOption(Mod mod, string name, string description = "");
public bool ChangeOptionDescription(int optionIndex, string newDescription);
public bool ChangeOptionName(int optionIndex, string newName);
public IReadOnlyList<IModOption> Options { get; }
public bool IsOption { get; }
public IReadOnlyList<IModOption> Options { get; }
public IReadOnlyList<IModDataContainer> DataContainers { get; }
public bool IsOption { get; }
public IModGroup Convert(GroupType type);
public bool MoveOption(int optionIdxFrom, int optionIdxTo);
public int GetIndex();
public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations);
/// <summary> Ensure that a value is valid for a group. </summary>
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
{
private readonly DirectoryInfo _basePath;
private readonly IModGroup? _group;
private readonly int _groupIdx;
private readonly SubMod? _defaultMod;
private readonly bool _onlyAscii;
private readonly DirectoryInfo _basePath;
private readonly IModGroup? _group;
private readonly int _groupIdx;
private readonly DefaultSubMod? _defaultMod;
private readonly bool _onlyAscii;
public ModSaveGroup(Mod mod, int groupIdx, bool onlyAscii)
{
@ -61,7 +128,7 @@ public readonly struct ModSaveGroup : ISavable
_onlyAscii = onlyAscii;
}
public ModSaveGroup(DirectoryInfo basePath, SubMod @default, bool onlyAscii)
public ModSaveGroup(DirectoryInfo basePath, DefaultSubMod @default, bool onlyAscii)
{
_basePath = basePath;
_groupIdx = -1;
@ -77,42 +144,11 @@ public readonly struct ModSaveGroup : ISavable
using var j = new JsonTextWriter(writer);
j.Formatting = Formatting.Indented;
var serializer = new JsonSerializer { Formatting = Formatting.Indented };
j.WriteStartObject();
if (_groupIdx >= 0)
{
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;
}
}
_group!.WriteJson(j, serializer);
else
{
SubMod.WriteSubMod(j, serializer, _defaultMod!, _basePath, null);
}
IModDataContainer.WriteModData(j, serializer, _defaultMod!, _basePath);
j.WriteEndObject();
}
}

View file

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

View file

@ -1,4 +1,5 @@
using Dalamud.Interface.Internal.Notifications;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Classes;
@ -10,20 +11,30 @@ using Penumbra.String.Classes;
namespace Penumbra.Mods.Subclasses;
/// <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
=> GroupType.Multi;
public Mod Mod { get; set; } = mod;
public string Name { get; set; } = "Group";
public string Description { get; set; } = "A non-exclusive group of settings.";
public ModPriority Priority { get; set; }
public Setting DefaultSettings { get; set; }
public Mod Mod { get; set; } = mod;
public string Name { get; set; } = "Group";
public string Description { get; set; } = "A non-exclusive group of settings.";
public ModPriority Priority { get; set; }
public Setting DefaultSettings { get; set; }
public 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)
=> PrioritizedOptions.OrderByDescending(o => o.Priority)
.SelectWhere(o => (o.Mod.FileData.TryGetValue(gamePath, out var file) || o.Mod.FileSwapData.TryGetValue(gamePath, out file), file))
=> OptionData.OrderByDescending(o => o.Priority)
.SelectWhere(o => (o.Files.TryGetValue(gamePath, out var file) || o.FileSwaps.TryGetValue(gamePath, out file), file))
.FirstOrDefault();
public int AddOption(Mod mod, string name, string description = "")
@ -32,49 +43,15 @@ public sealed class MultiModGroup(Mod mod) : IModGroup
if (groupIdx < 0)
return -1;
var subMod = new SubMod(mod, this)
var subMod = new MultiSubMod(mod, this)
{
Name = name,
Description = description,
};
PrioritizedOptions.Add((subMod, ModPriority.Default));
return PrioritizedOptions.Count - 1;
OptionData.Add(subMod);
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)
{
var ret = new MultiModGroup(mod)
@ -91,7 +68,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup
if (options != null)
foreach (var child in options.Children())
{
if (ret.PrioritizedOptions.Count == IModGroup.MaxMultiOptions)
if (ret.OptionData.Count == IModGroup.MaxMultiOptions)
{
Penumbra.Messager.NotificationMessage(
$"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;
}
var subMod = new SubMod(mod, ret);
subMod.Load(mod.ModPath, child, out var priority);
ret.PrioritizedOptions.Add((subMod, priority));
var subMod = new MultiSubMod(mod, ret, child);
ret.OptionData.Add(subMod);
}
ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings);
@ -115,39 +91,68 @@ public sealed class MultiModGroup(Mod mod) : IModGroup
{
case GroupType.Multi: return this;
case GroupType.Single:
var multi = new SingleModGroup(Mod)
var single = new SingleModGroup(Mod)
{
Name = Name,
Description = Description,
Priority = Priority,
DefaultSettings = DefaultSettings.TurnMulti(PrioritizedOptions.Count),
DefaultSettings = DefaultSettings.TurnMulti(OptionData.Count),
};
multi.OptionData.AddRange(PrioritizedOptions.Select(p => p.Mod));
return multi;
single.OptionData.AddRange(OptionData.Select(o => o.ConvertToSingle(Mod, single)));
return single;
default: throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
}
public bool MoveOption(int optionIdxFrom, int optionIdxTo)
{
if (!PrioritizedOptions.Move(optionIdxFrom, optionIdxTo))
if (!OptionData.Move(optionIdxFrom, optionIdxTo))
return false;
DefaultSettings = DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo);
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)
{
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))
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)
=> 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>
internal static MultiModGroup CreateForSaving(string name)
@ -155,4 +160,7 @@ public sealed class MultiModGroup(Mod mod) : IModGroup
{
Name = name,
};
IReadOnlyList<IModDataOption> ITexToolsGroup.OptionData
=> OptionData;
}

View file

@ -1,3 +1,4 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Filesystem;
@ -8,7 +9,7 @@ using Penumbra.String.Classes;
namespace Penumbra.Mods.Subclasses;
/// <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
=> GroupType.Single;
@ -19,16 +20,19 @@ public sealed class SingleModGroup(Mod mod) : IModGroup
public ModPriority Priority { 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)
=> 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();
public int AddOption(Mod mod, string name, string description = "")
{
var subMod = new SubMod(mod, this)
var subMod = new SingleSubMod(mod, this)
{
Name = name,
Description = description,
@ -37,35 +41,12 @@ public sealed class SingleModGroup(Mod mod) : IModGroup
return OptionData.Count - 1;
}
public bool ChangeOptionDescription(int optionIndex, string newDescription)
{
if (optionIndex < 0 || optionIndex >= OptionData.Count)
return false;
var option = OptionData[optionIndex];
if (option.Description == newDescription)
return false;
option.Description = newDescription;
return true;
}
public bool ChangeOptionName(int optionIndex, string newName)
{
if (optionIndex < 0 || optionIndex >= OptionData.Count)
return false;
var option = OptionData[optionIndex];
if (option.Name == newName)
return false;
option.Name = newName;
return true;
}
public IReadOnlyList<IModOption> Options
=> OptionData;
public IReadOnlyList<IModDataContainer> DataContainers
=> OptionData;
public bool IsOption
=> OptionData.Count > 1;
@ -85,8 +66,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup
if (options != null)
foreach (var child in options.Children())
{
var subMod = new SubMod(mod, ret);
subMod.Load(mod.ModPath, child, out _);
var subMod = new SingleSubMod(mod, ret, child);
ret.OptionData.Add(subMod);
}
@ -107,7 +87,7 @@ public sealed class SingleModGroup(Mod mod) : IModGroup
Priority = Priority,
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;
default: throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
@ -137,12 +117,39 @@ public sealed class SingleModGroup(Mod mod) : IModGroup
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)
=> OptionData[setting.AsIndex].AddData(redirections, manipulations);
=> OptionData[setting.AsIndex].AddDataTo(redirections, manipulations);
public Setting FixSetting(Setting setting)
=> 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>
internal static SingleModGroup CreateForSaving(string name)
=> new(null!)

View file

@ -7,7 +7,9 @@ using Penumbra.String.Classes;
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 SingleModGroup Group = group;
@ -19,12 +21,68 @@ public class SingleSubMod(Mod mod, SingleModGroup group) : IModOption, IModDataC
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> FileSwaps { 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 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> FileSwaps { 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 string FullName
=> "Default Option";
public const string FullName = "Default Option";
public string Description
=> string.Empty;
@ -55,183 +177,176 @@ public class DefaultSubMod(IMod mod) : IModDataContainer
public Dictionary<Utf8GamePath, FullPath> Files { get; set; } = [];
public Dictionary<Utf8GamePath, FullPath> FileSwaps { 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
=> Group == null ? "Default Option" : $"{Group.Name}: {Name}";
public string Description { get; set; } = string.Empty;
internal readonly IMod Mod = mod;
internal readonly IModGroup? Group = group;
internal (int GroupIdx, int OptionIdx) GetIndices()
{
if (IsDefault)
return (-1, 0);
var groupIdx = Mod.Groups.IndexOf(Group);
if (groupIdx < 0)
throw new Exception($"Group {Group.Name} from SubMod {Name} is not contained in Mod {Mod.Name}.");
return (groupIdx, GetOptionIndex());
}
private int GetOptionIndex()
{
var optionIndex = Group switch
{
null => 0,
SingleModGroup single => single.OptionData.IndexOf(this),
MultiModGroup multi => multi.PrioritizedOptions.IndexOf(p => p.Mod == this),
_ => throw new Exception($"Group {Group.Name} from SubMod {Name} has unknown type {typeof(Group)}"),
};
if (optionIndex < 0)
throw new Exception($"Group {Group!.Name} from SubMod {Name} does not contain this SubMod.");
return optionIndex;
}
public static SubMod CreateDefault(IMod mod)
=> new(mod, null!);
[MemberNotNullWhen(false, nameof(Group))]
public bool IsDefault
=> Group == null;
public void AddData(Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations)
{
foreach (var (path, file) in Files)
redirections.TryAdd(path, file);
foreach (var (path, file) in FileSwaps)
redirections.TryAdd(path, file);
manipulations.UnionWith(Manipulations);
}
public Dictionary<Utf8GamePath, FullPath> FileData = [];
public Dictionary<Utf8GamePath, FullPath> FileSwapData = [];
public HashSet<MetaManipulation> ManipulationData = [];
public IReadOnlyDictionary<Utf8GamePath, FullPath> Files
=> FileData;
public IReadOnlyDictionary<Utf8GamePath, FullPath> FileSwaps
=> FileSwapData;
public IReadOnlySet<MetaManipulation> Manipulations
=> ManipulationData;
public void Load(DirectoryInfo basePath, JToken json, out ModPriority priority)
{
FileData.Clear();
FileSwapData.Clear();
ManipulationData.Clear();
// Every option has a name, but priorities are only relevant for multi group options.
Name = json[nameof(Name)]?.ToObject<string>() ?? string.Empty;
Description = json[nameof(Description)]?.ToObject<string>() ?? string.Empty;
priority = json[nameof(IModGroup.Priority)]?.ToObject<ModPriority>() ?? ModPriority.Default;
var files = (JObject?)json[nameof(Files)];
if (files != null)
foreach (var property in files.Properties())
{
if (Utf8GamePath.FromString(property.Name, out var p, true))
FileData.TryAdd(p, new FullPath(basePath, property.Value.ToObject<Utf8RelPath>()));
}
var swaps = (JObject?)json[nameof(FileSwaps)];
if (swaps != null)
foreach (var property in swaps.Properties())
{
if (Utf8GamePath.FromString(property.Name, out var p, true))
FileSwapData.TryAdd(p, new FullPath(property.Value.ToObject<string>()!));
}
var manips = json[nameof(Manipulations)];
if (manips != null)
foreach (var s in manips.Children().Select(c => c.ToObject<MetaManipulation>())
.Where(m => m.Validate()))
ManipulationData.Add(s);
}
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}");
}
}
}
/// <summary> Create a sub mod without a mod or group only for saving it in the creator. </summary>
internal static SubMod CreateForSaving(string name)
=> new(null!, null!)
{
Name = name,
};
public static void WriteSubMod(JsonWriter j, JsonSerializer serializer, SubMod mod, DirectoryInfo basePath, ModPriority? priority)
{
j.WriteStartObject();
j.WritePropertyName(nameof(Name));
j.WriteValue(mod.Name);
j.WritePropertyName(nameof(Description));
j.WriteValue(mod.Description);
if (priority != null)
{
j.WritePropertyName(nameof(IModGroup.Priority));
j.WriteValue(priority.Value.Value);
}
j.WritePropertyName(nameof(mod.Files));
j.WriteStartObject();
foreach (var (gamePath, file) in mod.Files)
{
if (file.ToRelPath(basePath, out var relPath))
{
j.WritePropertyName(gamePath.ToString());
j.WriteValue(relPath.ToString());
}
}
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();
}
}
//public sealed class SubMod(IMod mod, IModGroup group) : IModOption
//{
// public string Name { get; set; } = "Default";
//
// public string FullName
// => Group == null ? "Default Option" : $"{Group.Name}: {Name}";
//
// public string Description { get; set; } = string.Empty;
//
// internal readonly IMod Mod = mod;
// internal readonly IModGroup? Group = group;
//
// internal (int GroupIdx, int OptionIdx) GetIndices()
// {
// if (IsDefault)
// return (-1, 0);
//
// var groupIdx = Mod.Groups.IndexOf(Group);
// if (groupIdx < 0)
// throw new Exception($"Group {Group.Name} from SubMod {Name} is not contained in Mod {Mod.Name}.");
//
// return (groupIdx, GetOptionIndex());
// }
//
// private int GetOptionIndex()
// {
// var optionIndex = Group switch
// {
// null => 0,
// SingleModGroup single => single.OptionData.IndexOf(this),
// MultiModGroup multi => multi.OptionData.IndexOf(p => p.Mod == this),
// _ => throw new Exception($"Group {Group.Name} from SubMod {Name} has unknown type {typeof(Group)}"),
// };
// if (optionIndex < 0)
// throw new Exception($"Group {Group!.Name} from SubMod {Name} does not contain this SubMod.");
//
// return optionIndex;
// }
//
// public static SubMod CreateDefault(IMod mod)
// => new(mod, null!);
//
// [MemberNotNullWhen(false, nameof(Group))]
// public bool IsDefault
// => Group == null;
//
// public void AddData(Dictionary<Utf8GamePath, FullPath> redirections, HashSet<MetaManipulation> manipulations)
// {
// foreach (var (path, file) in Files)
// redirections.TryAdd(path, file);
//
// foreach (var (path, file) in FileSwaps)
// redirections.TryAdd(path, file);
// manipulations.UnionWith(Manipulations);
// }
//
// public Dictionary<Utf8GamePath, FullPath> FileData = [];
// public Dictionary<Utf8GamePath, FullPath> FileSwapData = [];
// public HashSet<MetaManipulation> ManipulationData = [];
//
// public IReadOnlyDictionary<Utf8GamePath, FullPath> Files
// => FileData;
//
// public IReadOnlyDictionary<Utf8GamePath, FullPath> FileSwaps
// => FileSwapData;
//
// public IReadOnlySet<MetaManipulation> Manipulations
// => ManipulationData;
//
// public void Load(DirectoryInfo basePath, JToken json, out ModPriority priority)
// {
// FileData.Clear();
// FileSwapData.Clear();
// ManipulationData.Clear();
//
// // Every option has a name, but priorities are only relevant for multi group options.
// Name = json[nameof(Name)]?.ToObject<string>() ?? string.Empty;
// Description = json[nameof(Description)]?.ToObject<string>() ?? string.Empty;
// priority = json[nameof(IModGroup.Priority)]?.ToObject<ModPriority>() ?? ModPriority.Default;
//
// var files = (JObject?)json[nameof(Files)];
// if (files != null)
// foreach (var property in files.Properties())
// {
// if (Utf8GamePath.FromString(property.Name, out var p, true))
// FileData.TryAdd(p, new FullPath(basePath, property.Value.ToObject<Utf8RelPath>()));
// }
//
// var swaps = (JObject?)json[nameof(FileSwaps)];
// if (swaps != null)
// foreach (var property in swaps.Properties())
// {
// if (Utf8GamePath.FromString(property.Name, out var p, true))
// FileSwapData.TryAdd(p, new FullPath(property.Value.ToObject<string>()!));
// }
//
// var manips = json[nameof(Manipulations)];
// if (manips != null)
// foreach (var s in manips.Children().Select(c => c.ToObject<MetaManipulation>())
// .Where(m => m.Validate()))
// ManipulationData.Add(s);
// }
//
//
// /// <summary> Create a sub mod without a mod or group only for saving it in the creator. </summary>
// internal static SubMod CreateForSaving(string name)
// => new(null!, null!)
// {
// Name = name,
// };
//
//
// public static void WriteSubMod(JsonWriter j, JsonSerializer serializer, SubMod mod, DirectoryInfo basePath, ModPriority? priority)
// {
// j.WriteStartObject();
// j.WritePropertyName(nameof(Name));
// j.WriteValue(mod.Name);
// j.WritePropertyName(nameof(Description));
// j.WriteValue(mod.Description);
// if (priority != null)
// {
// j.WritePropertyName(nameof(IModGroup.Priority));
// j.WriteValue(priority.Value.Value);
// }
//
// j.WritePropertyName(nameof(mod.Files));
// j.WriteStartObject();
// foreach (var (gamePath, file) in mod.Files)
// {
// if (file.ToRelPath(basePath, out var relPath))
// {
// j.WritePropertyName(gamePath.ToString());
// j.WriteValue(relPath.ToString());
// }
// }
//
// 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
=> Default.Manipulations.Count;
public readonly SubMod Default;
public readonly DefaultSubMod Default;
public AppliedModData GetData(ModSettings? settings = null)
{
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
{
// Need to ensure uniqueness.
dict = new Dictionary<Utf8GamePath, FullPath>(Default.FileData.Count + Default.FileSwaps.Count);
foreach (var (gamePath, file) in Default.FileData.Concat(Default.FileSwaps))
dict = new Dictionary<Utf8GamePath, FullPath>(Default.Files.Count + Default.FileSwaps.Count);
foreach (var (gamePath, file) in Default.Files.Concat(Default.FileSwaps))
dict.TryAdd(gamePath, file);
}
return new AppliedModData(dict, Default.ManipulationData);
return new AppliedModData(dict, Default.Manipulations);
}
public IReadOnlyList<IModGroup> Groups
=> Array.Empty<IModGroup>();
public IEnumerable<SubMod> AllSubMods
=> [Default];
public TemporaryMod()
=> Default = SubMod.CreateDefault(this);
=> Default = new(this);
public void SetFile(Utf8GamePath gamePath, FullPath fullPath)
=> Default.FileData[gamePath] = fullPath;
=> Default.Files[gamePath] = fullPath;
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)
{
Default.FileData = dict;
Default.ManipulationData = manips;
Default.Files = dict;
Default.Manipulations = manips;
}
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));
File.Copy(targetPath, target, true);
defaultMod.FileData[gamePath] = new FullPath(target);
defaultMod.Files[gamePath] = new FullPath(target);
}
else
{
defaultMod.FileSwapData[gamePath] = new FullPath(targetPath);
defaultMod.FileSwaps[gamePath] = new FullPath(targetPath);
}
}
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));
modManager.AddMod(dir);

View file

@ -305,7 +305,7 @@ public class FileEditor<T>(
UiHelpers.Text(gamePath.Path);
ImGui.TableNextColumn();
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.Classes;
using OtterGui.Raii;
using Penumbra.Mods;
using Penumbra.Mods.Editor;
using Penumbra.Mods.Subclasses;
using Penumbra.String.Classes;
@ -79,7 +78,7 @@ public partial class ModEditWindow
var file = f.RelPath.ToString();
return f.SubModUsage.Count == 0
? 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));
});
@ -148,13 +147,13 @@ public partial class ModEditWindow
(string, int) GetMulti()
{
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
{
ColorId.ConflictingMod => (string.Empty, 0),
ColorId.NewMod => (registry.SubModUsage[0].Item1.Name, 1),
ColorId.NewMod => (registry.SubModUsage[0].Item1.GetName(), 1),
ColorId.InheritedMod => GetMulti(),
_ => (string.Empty, 0),
};
@ -192,7 +191,7 @@ public partial class ModEditWindow
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);
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 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.";
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)
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.";
ImGui.NewLine();
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();
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?
// 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))
.Where(kv => kv.Value.FullName.Equals(path, StringComparison.OrdinalIgnoreCase))
.Select(kv => kv.Key)
@ -103,7 +103,7 @@ public partial class ModEditWindow
return [];
// Filter then prepend the current option to ensure it's chosen first.
return mod.AllSubMods
return mod.AllDataContainers
.Where(subMod => subMod != option)
.Prepend(option)
.SelectMany(subMod => subMod.Manipulations)

View file

@ -187,8 +187,8 @@ public partial class ModEditWindow
if (editor == null)
return new QuickImportAction(owner._editor, FallbackOptionName, gamePath);
var subMod = editor.Option;
var optionName = subMod!.FullName;
var subMod = editor.Option!;
var optionName = subMod is IModOption o ? o.FullName : FallbackOptionName;
if (gamePath.IsEmpty || file == null || editor.FileEditor.Changes)
return new QuickImportAction(editor, optionName, gamePath);
@ -199,7 +199,7 @@ public partial class ModEditWindow
if (mod == null)
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;
if (File.Exists(targetPath))
return new QuickImportAction(editor, optionName, gamePath);
@ -222,16 +222,16 @@ public partial class ModEditWindow
{
fileRegistry,
}, _subDirs);
_editor.FileEditor.Apply(_editor.Mod!, (SubMod)_editor.Option!);
_editor.FileEditor.Apply(_editor.Mod!, _editor.Option!);
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 subDirs = 0;
if (subMod == mod.Default)
if (subMod == null)
return (path, subDirs);
var name = subMod.Name;

View file

@ -77,10 +77,10 @@ public partial class ModEditWindow : Window, IDisposable
_forceTextureStartPath = true;
}
public void ChangeOption(SubMod? subMod)
public void ChangeOption(IModDataContainer? subMod)
{
var (groupIdx, optionIdx) = subMod?.GetIndices() ?? (-1, 0);
_editor.LoadOption(groupIdx, optionIdx);
var (groupIdx, dataIdx) = subMod?.GetDataIndices() ?? (-1, 0);
_editor.LoadOption(groupIdx, dataIdx);
}
public void UpdateModels()
@ -111,7 +111,7 @@ public partial class ModEditWindow : Window, IDisposable
});
var manipulations = 0;
var subMods = 0;
var swaps = Mod!.AllSubMods.Sum(m =>
var swaps = Mod!.AllDataContainers.Sum(m =>
{
++subMods;
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))
{
_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)
@ -405,7 +405,7 @@ public partial class ModEditWindow : Window, IDisposable
var width = new Vector2(ImGui.GetContentRegionAvail().X / 3, 0);
var ret = false;
if (ImGuiUtil.DrawDisabledButton(defaultOption, width, "Switch to the default option for the mod.\nThis resets unsaved changes.",
_editor.Option!.IsDefault))
_editor.Option is DefaultSubMod))
{
_editor.LoadOption(-1, 0);
ret = true;
@ -414,7 +414,7 @@ public partial class ModEditWindow : Window, IDisposable
ImGui.SameLine();
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;
}
@ -422,17 +422,17 @@ public partial class ModEditWindow : Window, IDisposable
ImGui.SetNextItemWidth(width.X);
style.Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale);
using var color = ImRaii.PushColor(ImGuiCol.Border, ColorId.FolderLine.Value());
using var combo = ImRaii.Combo("##optionSelector", _editor.Option.FullName);
using var combo = ImRaii.Combo("##optionSelector", _editor.Option!.GetFullName());
if (!combo)
return ret;
foreach (var (option, idx) in Mod!.AllSubMods.WithIndex())
foreach (var (option, idx) in Mod!.AllDataContainers.WithIndex())
{
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();
_editor.LoadOption(groupIdx, optionIdx);
var (groupIdx, dataIdx) = option.GetDataIndices();
_editor.LoadOption(groupIdx, dataIdx);
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.";
ImGui.NewLine();
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();
tt = setsEqual ? "No changes staged." : "Revert all currently staged changes.";
@ -569,7 +569,7 @@ public partial class ModEditWindow : Window, IDisposable
}
if (Mod != null)
foreach (var option in Mod.AllSubMods)
foreach (var option in Mod.AllDataContainers)
{
foreach (var path in option.Files.Keys)
{

View file

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

View file

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

View file

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