From d403f4425627b6dfaf8285656eecd35430fde0d5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 5 May 2023 16:19:08 +0200 Subject: [PATCH] Add start of mod merger. --- Penumbra/Communication/ModPathChanged.cs | 5 +- Penumbra/Mods/Editor/DuplicateManager.cs | 20 +- Penumbra/Mods/Editor/MdlMaterialEditor.cs | 1 + Penumbra/Mods/Editor/ModEditor.cs | 1 + Penumbra/Mods/Editor/ModFileCollection.cs | 3 +- Penumbra/Mods/Editor/ModFileEditor.cs | 1 + Penumbra/Mods/Editor/ModMerger.cs | 361 ++++++++++++++++++++ Penumbra/Mods/Manager/ModOptionEditor.cs | 31 +- Penumbra/Mods/Manager/ModStorage.cs | 15 + Penumbra/Services/ServiceManager.cs | 6 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 10 +- Penumbra/UI/AdvancedWindow/ModMergeTab.cs | 104 ++++++ 12 files changed, 538 insertions(+), 20 deletions(-) create mode 100644 Penumbra/Mods/Editor/ModMerger.cs create mode 100644 Penumbra/UI/AdvancedWindow/ModMergeTab.cs diff --git a/Penumbra/Communication/ModPathChanged.cs b/Penumbra/Communication/ModPathChanged.cs index 608c10eb..0a362267 100644 --- a/Penumbra/Communication/ModPathChanged.cs +++ b/Penumbra/Communication/ModPathChanged.cs @@ -37,13 +37,14 @@ public sealed class ModPathChanged : EventWrapper ModManager = 0, + /// + ModMerger = 0, + /// CollectionStorage = 10, /// CollectionCacheManagerRemoval = 100, - - } public ModPathChanged() : base(nameof(ModPathChanged)) diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index 62edb9fb..03321cc3 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -8,22 +8,19 @@ using System.Threading.Tasks; using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.String.Classes; -using Penumbra.Util; -namespace Penumbra.Mods; +namespace Penumbra.Mods.Editor; public class DuplicateManager { private readonly SaveService _saveService; private readonly ModManager _modManager; private readonly SHA256 _hasher = SHA256.Create(); - private readonly ModFileCollection _files; private readonly List<(FullPath[] Paths, long Size, byte[] Hash)> _duplicates = new(); - public DuplicateManager(ModFileCollection files, ModManager modManager, SaveService saveService) + public DuplicateManager(ModManager modManager, SaveService saveService) { - _files = files; - _modManager = modManager; + _modManager = modManager; _saveService = saveService; } @@ -43,7 +40,7 @@ public class DuplicateManager Task.Run(() => CheckDuplicates(filesTmp)); } - public void DeleteDuplicates(Mod mod, ISubMod option, bool useModManager) + public void DeleteDuplicates(ModFileCollection files, Mod mod, ISubMod option, bool useModManager) { if (!Finished || _duplicates.Count == 0) return; @@ -60,7 +57,7 @@ public class DuplicateManager _duplicates.Clear(); DeleteEmptyDirectories(mod.ModPath); - _files.UpdateAll(mod, option); + files.UpdateAll(mod, option); } public void Clear() @@ -248,9 +245,10 @@ public class DuplicateManager _modManager.Creator.ReloadMod(mod, true, out _); Finished = false; - _files.UpdateAll(mod, mod.Default); - CheckDuplicates(_files.Available.OrderByDescending(f => f.FileSize).ToArray()); - DeleteDuplicates(mod, mod.Default, false); + var files = new ModFileCollection(); + files.UpdateAll(mod, mod.Default); + CheckDuplicates(files.Available.OrderByDescending(f => f.FileSize).ToArray()); + DeleteDuplicates(files, mod, mod.Default, false); } catch (Exception e) { diff --git a/Penumbra/Mods/Editor/MdlMaterialEditor.cs b/Penumbra/Mods/Editor/MdlMaterialEditor.cs index dc32869f..f616d128 100644 --- a/Penumbra/Mods/Editor/MdlMaterialEditor.cs +++ b/Penumbra/Mods/Editor/MdlMaterialEditor.cs @@ -7,6 +7,7 @@ using System.Text.RegularExpressions; using OtterGui; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; +using Penumbra.Mods.Editor; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Editor/ModEditor.cs b/Penumbra/Mods/Editor/ModEditor.cs index c19b9962..a874f629 100644 --- a/Penumbra/Mods/Editor/ModEditor.cs +++ b/Penumbra/Mods/Editor/ModEditor.cs @@ -1,6 +1,7 @@ using System; using System.IO; using OtterGui; +using Penumbra.Mods.Editor; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Editor/ModFileCollection.cs b/Penumbra/Mods/Editor/ModFileCollection.cs index fa3d5614..5ef290dc 100644 --- a/Penumbra/Mods/Editor/ModFileCollection.cs +++ b/Penumbra/Mods/Editor/ModFileCollection.cs @@ -3,11 +3,10 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; -using Microsoft.Win32; using OtterGui; using Penumbra.String.Classes; -namespace Penumbra.Mods; +namespace Penumbra.Mods.Editor; public class ModFileCollection : IDisposable { diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index 17505550..4f9d22b3 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.String.Classes; diff --git a/Penumbra/Mods/Editor/ModMerger.cs b/Penumbra/Mods/Editor/ModMerger.cs new file mode 100644 index 00000000..29ed210e --- /dev/null +++ b/Penumbra/Mods/Editor/ModMerger.cs @@ -0,0 +1,361 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Utility; +using OtterGui; +using Penumbra.Api.Enums; +using Penumbra.Communication; +using Penumbra.Mods.Manager; +using Penumbra.Services; +using Penumbra.String.Classes; +using Penumbra.UI.ModsTab; + +namespace Penumbra.Mods.Editor; + +public class ModMerger : IDisposable +{ + private readonly CommunicatorService _communicator; + private readonly ModOptionEditor _editor; + private readonly ModFileSystemSelector _selector; + private readonly DuplicateManager _duplicates; + private readonly ModManager _mods; + private readonly ModCreator _creator; + + public Mod? MergeFromMod { get; private set; } + public Mod? MergeToMod; + public string OptionGroupName = "Merges"; + public string OptionName = string.Empty; + + + private readonly Dictionary _fileToFile = new(); + private readonly HashSet _createdDirectories = new(); + public readonly HashSet SelectedOptions = new(); + + private int _createdGroup = -1; + private SubMod? _createdOption; + public Exception? Error { get; private set; } + + public ModMerger(ModManager mods, ModOptionEditor editor, ModFileSystemSelector selector, DuplicateManager duplicates, + CommunicatorService communicator, ModCreator creator) + { + _editor = editor; + _selector = selector; + _duplicates = duplicates; + _communicator = communicator; + _creator = creator; + _mods = mods; + _selector.SelectionChanged += OnSelectionChange; + _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.Api); + } + + public void Dispose() + { + _selector.SelectionChanged -= OnSelectionChange; + _communicator.ModPathChanged.Unsubscribe(OnModPathChange); + } + + public IEnumerable ModsWithoutCurrent + => _mods.Where(m => m != MergeFromMod); + + public bool CanMerge + => MergeToMod != null && MergeToMod != MergeFromMod && !MergeFromMod!.HasOptions; + + public void Merge() + { + if (MergeFromMod == null || MergeToMod == null || MergeFromMod == MergeToMod) + return; + + try + { + Error = null; + DataCleanup(); + if (MergeFromMod.HasOptions) + MergeWithOptions(); + else + MergeIntoOption(OptionGroupName, OptionName); + _duplicates.DeduplicateMod(MergeToMod.ModPath); + } + catch (Exception ex) + { + Error = ex; + Penumbra.ChatService.NotificationMessage( + $"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}, cleaning up changes.:\n{ex}", "Failure", + NotificationType.Error); + FailureCleanup(); + DataCleanup(); + } + } + + private void MergeWithOptions() + { + // Not supported + } + + private void MergeIntoOption(string groupName, string optionName) + { + if (groupName.Length == 0 && optionName.Length == 0) + { + CopyFiles(MergeToMod!.ModPath); + MergeIntoOption(MergeFromMod!.AllSubMods.Reverse(), MergeToMod!.Default); + } + else if (groupName.Length * optionName.Length == 0) + { + return; + } + + var (group, groupIdx, groupCreated) = _editor.FindOrAddModGroup(MergeToMod!, GroupType.Multi, groupName); + if (groupCreated) + _createdGroup = groupIdx; + var (option, optionCreated) = _editor.FindOrAddOption(MergeToMod!, groupIdx, optionName); + if (optionCreated) + _createdOption = option; + var dir = ModCreator.NewOptionDirectory(MergeToMod!.ModPath, groupName); + if (!dir.Exists) + _createdDirectories.Add(dir.FullName); + dir = ModCreator.NewOptionDirectory(dir, optionName); + if (!dir.Exists) + _createdDirectories.Add(dir.FullName); + CopyFiles(dir); + MergeIntoOption(MergeFromMod!.AllSubMods.Reverse(), option); + } + + private void MergeIntoOption(IEnumerable mergeOptions, SubMod option) + { + 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(); + foreach (var originalOption in mergeOptions) + { + foreach (var manip in originalOption.Manipulations) + { + 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."); + } + + 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."); + } + + foreach (var (gamePath, relPath) in originalOption.Files) + { + if (!_fileToFile.TryGetValue(relPath.FullName, out var newFile)) + throw new Exception( + $"Could not add file redirection {relPath} -> {gamePath} from {originalOption.FullName} to {option.FullName} because the file does not exist in the new mod."); + if (!redirections.TryAdd(gamePath, new FullPath(newFile))) + throw new Exception( + $"Could not add file redirection {relPath} -> {gamePath} from {originalOption.FullName} to {option.FullName} because a redirection for the game path already exists."); + } + } + + _editor.OptionSetFiles(MergeToMod!, option.GroupIdx, option.OptionIdx, redirections); + _editor.OptionSetFileSwaps(MergeToMod!, option.GroupIdx, option.OptionIdx, swaps); + _editor.OptionSetManipulations(MergeToMod!, option.GroupIdx, option.OptionIdx, manips); + } + + private void CopyFiles(DirectoryInfo directory) + { + directory = Directory.CreateDirectory(directory.FullName); + foreach (var file in MergeFromMod!.ModPath.EnumerateDirectories() + .Where(d => !d.IsHidden()) + .SelectMany(FileExtensions.EnumerateNonHiddenFiles)) + { + var path = Path.GetRelativePath(MergeFromMod.ModPath.FullName, file.FullName); + path = Path.Combine(directory.FullName, path); + var finalDir = Path.GetDirectoryName(path)!; + var dir = finalDir; + while (!dir.IsNullOrEmpty()) + { + if (!Directory.Exists(dir)) + _createdDirectories.Add(dir); + else + break; + + dir = Path.GetDirectoryName(dir); + } + + Directory.CreateDirectory(finalDir); + file.CopyTo(path); + Penumbra.Log.Verbose($"[Merger] Copied file {file.FullName} to {path}."); + _fileToFile.Add(file.FullName, path); + } + } + + public void SplitIntoMod(string modName) + { + var mods = SelectedOptions.ToList(); + if (mods.Count == 0) + return; + + Error = null; + DirectoryInfo? dir = null; + Mod? result = null; + try + { + dir = _creator.CreateEmptyMod(_mods.BasePath, modName, $"Split off from {mods[0].ParentMod.Name}."); + if (dir == null) + throw new Exception($"Could not split off mods, unable to create new mod with name {modName}."); + + _mods.AddMod(dir); + result = _mods[^1]; + if (mods.Count == 1) + { + 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); + } + else + { + foreach (var originalOption in mods) + { + var originalGroup = originalOption.ParentMod.Groups[originalOption.GroupIdx]; + if (originalOption.IsDefault) + { + 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); + } + else + { + var (group, groupIdx, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name); + var (option, _) = _editor.FindOrAddOption(result, groupIdx, originalOption.Name); + var folder = Path.Combine(dir.FullName, group.Name, option.Name); + var files = CopySubModFiles(originalOption, new DirectoryInfo(folder)); + _editor.OptionSetFiles(result, groupIdx, option.OptionIdx, files); + _editor.OptionSetFileSwaps(result, groupIdx, option.OptionIdx, originalOption.FileSwapData); + _editor.OptionSetManipulations(result, groupIdx, option.OptionIdx, originalOption.ManipulationData); + } + } + } + } + catch (Exception e) + { + Error = e; + if (result != null) + _mods.DeleteMod(result); + else if (dir != null) + try + { + Directory.Delete(dir.FullName); + } + catch (Exception ex) + { + Penumbra.Log.Error($"Could not clean up after failure to split options into new mod {modName}:\n{ex}"); + } + } + } + + private static Dictionary CopySubModFiles(SubMod option, DirectoryInfo newMod) + { + var ret = new Dictionary(option.FileData.Count); + var parentPath = ((Mod)option.ParentMod).ModPath.FullName; + foreach (var (path, file) in option.FileData) + { + var target = Path.GetRelativePath(parentPath, file.FullName); + target = Path.Combine(newMod.FullName, target); + Directory.CreateDirectory(Path.GetDirectoryName(target)!); + File.Copy(file.FullName, target); + Penumbra.Log.Verbose($"[Splitter] Copied file {file.FullName} to {target}."); + ret.Add(path, new FullPath(target)); + } + + return ret; + } + + private void DataCleanup() + { + _fileToFile.Clear(); + _createdDirectories.Clear(); + _createdOption = null; + _createdGroup = -1; + } + + private void FailureCleanup() + { + if (_createdGroup >= 0 && _createdGroup < MergeToMod!.Groups.Count) + _editor.DeleteModGroup(MergeToMod!, _createdGroup); + else if (_createdOption != null) + _editor.DeleteOption(MergeToMod!, _createdOption.GroupIdx, _createdOption.OptionIdx); + + foreach (var dir in _createdDirectories) + { + if (!Directory.Exists(dir)) + continue; + + try + { + Directory.Delete(dir, true); + Penumbra.Log.Verbose($"[Merger] Deleted {dir}."); + } + catch (Exception ex) + { + Penumbra.Log.Error( + $"Could not clean up after failing to merge {MergeFromMod!.Name} into {MergeToMod!.Name}, unable to delete {dir}:\n{ex}"); + } + } + + foreach (var (_, file) in _fileToFile) + { + if (!File.Exists(file)) + continue; + + try + { + File.Delete(file); + Penumbra.Log.Verbose($"[Merger] Deleted {file}."); + } + catch (Exception ex) + { + Penumbra.Log.Error( + $"Could not clean up after failing to merge {MergeFromMod!.Name} into {MergeToMod!.Name}, unable to delete {file}:\n{ex}"); + } + } + } + + private void OnSelectionChange(Mod? oldSelection, Mod? newSelection, in ModFileSystemSelector.ModState state) + { + if (OptionGroupName == "Merges" && OptionName.Length == 0 || OptionName == oldSelection?.Name.Text) + OptionName = newSelection?.Name.Text ?? string.Empty; + + if (MergeToMod == newSelection) + MergeToMod = null; + + SelectedOptions.Clear(); + MergeFromMod = newSelection; + } + + private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? _1, DirectoryInfo? _2) + { + switch (type) + { + case ModPathChangeType.Deleted: + { + if (mod == MergeFromMod) + { + SelectedOptions.Clear(); + MergeFromMod = null; + } + + if (mod == MergeToMod) + MergeToMod = null; + break; + } + case ModPathChangeType.StartingReload: + SelectedOptions.Clear(); + MergeFromMod = null; + MergeToMod = null; + break; + case ModPathChangeType.Reloaded: + MergeFromMod = _selector.Selected; + break; + } + } +} diff --git a/Penumbra/Mods/Manager/ModOptionEditor.cs b/Penumbra/Mods/Manager/ModOptionEditor.cs index c30d90c3..a5e77c37 100644 --- a/Penumbra/Mods/Manager/ModOptionEditor.cs +++ b/Penumbra/Mods/Manager/ModOptionEditor.cs @@ -87,7 +87,7 @@ public class ModOptionEditor _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1); } - /// Add a new mod, empty option group of the given type and name. + /// Add a new, empty option group of the given type and name. public void AddModGroup(Mod mod, GroupType type, string newName) { if (!VerifyFileName(mod, null, newName, true)) @@ -110,6 +110,20 @@ public class ModOptionEditor _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod.Groups.Count - 1, -1, -1); } + /// Add a new mod, empty option group of the given type and name if it does not exist already. + public (IModGroup, int, bool) FindOrAddModGroup(Mod mod, GroupType type, string newName) + { + var idx = mod.Groups.IndexOf(g => g.Name == newName); + if (idx >= 0) + return (mod.Groups[idx], idx, false); + + AddModGroup(mod, type, newName); + if (mod.Groups[^1].Name != newName) + throw new Exception($"Could not create new mod group with name {newName}."); + + return (mod.Groups[^1], mod.Groups.Count - 1, true); + } + /// Delete a given option group. Fires an event to prepare before actually deleting. public void DeleteModGroup(Mod mod, int groupIdx) { @@ -242,6 +256,21 @@ public class ModOptionEditor _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); } + /// Add a new empty option of the given name for the given group if it does not exist already. + public (SubMod, bool) FindOrAddOption(Mod mod, int groupIdx, string newName) + { + var group = mod.Groups[groupIdx]; + var idx = group.IndexOf(o => o.Name == newName); + if (idx >= 0) + return ((SubMod)group[idx], false); + + AddOption(mod, groupIdx, newName); + if (group[^1].Name != newName) + throw new Exception($"Could not create new option with name {newName} in {group.Name}."); + + return ((SubMod)group[^1], true); + } + /// Add an existing option to a given group with a given priority. public void AddOption(Mod mod, int groupIdx, ISubMod option, int priority = 0) { diff --git a/Penumbra/Mods/Manager/ModStorage.cs b/Penumbra/Mods/Manager/ModStorage.cs index 5e8999d7..8421d6e2 100644 --- a/Penumbra/Mods/Manager/ModStorage.cs +++ b/Penumbra/Mods/Manager/ModStorage.cs @@ -2,9 +2,24 @@ using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using OtterGui.Classes; +using OtterGui.Widgets; namespace Penumbra.Mods.Manager; +public class ModCombo : FilterComboCache +{ + protected override bool IsVisible(int globalIndex, LowerString filter) + => Items[globalIndex].Name.Contains(filter); + + protected override string ToString(Mod obj) + => obj.Name.Text; + + public ModCombo(Func> generator) + : base(generator) + { } +} + public class ModStorage : IReadOnlyList { /// The actual list of mods. diff --git a/Penumbra/Services/ServiceManager.cs b/Penumbra/Services/ServiceManager.cs index 6d1bf710..3b9e3e31 100644 --- a/Penumbra/Services/ServiceManager.cs +++ b/Penumbra/Services/ServiceManager.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using Dalamud.Plugin; using Microsoft.Extensions.DependencyInjection; using OtterGui.Classes; @@ -13,6 +14,7 @@ using Penumbra.Interop.ResourceTree; using Penumbra.Interop.Services; using Penumbra.Meta; using Penumbra.Mods; +using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.UI; using Penumbra.UI.AdvancedWindow; @@ -158,7 +160,8 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); private static IServiceCollection AddModEditor(this IServiceCollection services) => services.AddSingleton() @@ -168,6 +171,7 @@ public static class ServiceManager .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton(); private static IServiceCollection AddApi(this IServiceCollection services) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index ba6cc0aa..47d8f770 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -35,6 +35,7 @@ public partial class ModEditWindow : Window, IDisposable private readonly MetaFileManager _metaFileManager; private readonly ActiveCollections _activeCollections; private readonly StainService _stainService; + private readonly ModMergeTab _modMergeTab; private Mod? _mod; private Vector2 _iconSize = Vector2.Zero; @@ -144,6 +145,7 @@ public partial class ModEditWindow : Window, IDisposable DrawFileTab(); DrawMetaTab(); DrawSwapTab(); + _modMergeTab.Draw(); DrawMissingFilesTab(); DrawDuplicatesTab(); DrawQuickImportTab(); @@ -311,7 +313,7 @@ public partial class ModEditWindow : Window, IDisposable } if (ImGui.Button("Delete and Redirect Duplicates")) - _editor.Duplicates.DeleteDuplicates(_editor.Mod!, _editor.Option!, true); + _editor.Duplicates.DeleteDuplicates(_editor.Files, _editor.Mod!, _editor.Option!, true); if (_editor.Duplicates.SavedSpace > 0) { @@ -419,7 +421,8 @@ public partial class ModEditWindow : Window, IDisposable if (otherSwaps > 0) { ImGui.SameLine(); - ImGuiUtil.DrawTextButton($"There are {otherSwaps} file swaps configured in other options.", Vector2.Zero, ColorId.RedundantAssignment.Value()); + ImGuiUtil.DrawTextButton($"There are {otherSwaps} file swaps configured in other options.", Vector2.Zero, + ColorId.RedundantAssignment.Value()); } using var child = ImRaii.Child("##swaps", -Vector2.One, true); @@ -509,7 +512,7 @@ public partial class ModEditWindow : Window, IDisposable public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, DataManager gameData, Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, - StainService stainService, ActiveCollections activeCollections, UiBuilder uiBuilder, DalamudServices dalamud) + StainService stainService, ActiveCollections activeCollections, UiBuilder uiBuilder, DalamudServices dalamud, ModMergeTab modMergeTab) : base(WindowBaseLabel) { _performance = performance; @@ -520,6 +523,7 @@ public partial class ModEditWindow : Window, IDisposable _stainService = stainService; _activeCollections = activeCollections; _dalamud = dalamud; + _modMergeTab = modMergeTab; _fileDialog = fileDialog; _materialTab = new FileEditor(this, gameData, config, _fileDialog, "Materials", ".mtrl", () => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, diff --git a/Penumbra/UI/AdvancedWindow/ModMergeTab.cs b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs new file mode 100644 index 00000000..c4cf1fb4 --- /dev/null +++ b/Penumbra/UI/AdvancedWindow/ModMergeTab.cs @@ -0,0 +1,104 @@ +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.Mods.Editor; +using Penumbra.Mods.Manager; +using Penumbra.UI.Classes; +using SixLabors.ImageSharp.ColorSpaces; + +namespace Penumbra.UI.AdvancedWindow; + +public class ModMergeTab +{ + private readonly ModMerger _modMerger; + private readonly ModCombo _modCombo; + + private string _newModName = string.Empty; + + public ModMergeTab(ModMerger modMerger) + { + _modMerger = modMerger; + _modCombo = new ModCombo(() => _modMerger.ModsWithoutCurrent.ToList()); + } + + public void Draw() + { + if (_modMerger.MergeFromMod == null) + return; + + using var tab = ImRaii.TabItem("Merge Mods"); + if (!tab) + return; + + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted($"Merge {_modMerger.MergeFromMod.Name} into "); + ImGui.SameLine(); + DrawCombo(); + + ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); + ImGui.InputTextWithHint("##optionGroupInput", "Target Option Group", ref _modMerger.OptionGroupName, 64); + ImGui.SameLine(); + ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); + ImGui.InputTextWithHint("##optionInput", "Target Option Name", ref _modMerger.OptionName, 64); + + if (ImGuiUtil.DrawDisabledButton("Merge", Vector2.Zero, string.Empty, !_modMerger.CanMerge)) + _modMerger.Merge(); + + ImGui.Dummy(Vector2.One); + ImGui.Separator(); + ImGui.Dummy(Vector2.One); + using (var table = ImRaii.Table("##options", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.ScrollY, ImGui.GetContentRegionAvail() with { Y = 6 * ImGui.GetFrameHeightWithSpacing()})) + { + foreach (var (option, idx) in _modMerger.MergeFromMod.AllSubMods.WithIndex()) + { + using var id = ImRaii.PushId(idx); + var selected = _modMerger.SelectedOptions.Contains(option); + ImGui.TableNextColumn(); + if (ImGui.Checkbox("##check", ref selected)) + { + if (selected) + _modMerger.SelectedOptions.Add(option); + else + _modMerger.SelectedOptions.Remove(option); + } + + if (option.IsDefault) + { + ImGuiUtil.DrawTableColumn(option.FullName); + ImGui.TableNextColumn(); + } + else + { + ImGuiUtil.DrawTableColumn(option.ParentMod.Groups[option.GroupIdx].Name); + ImGuiUtil.DrawTableColumn(option.Name); + } + + ImGuiUtil.DrawTableColumn(option.FileData.Count.ToString()); + ImGuiUtil.DrawTableColumn(option.FileSwapData.Count.ToString()); + ImGuiUtil.DrawTableColumn(option.Manipulations.Count.ToString()); + + } + } + ImGui.InputTextWithHint("##newModInput", "New Mod Name...", ref _newModName, 64); + if (ImGuiUtil.DrawDisabledButton("Split Off", Vector2.Zero, string.Empty, _newModName.Length == 0 || _modMerger.SelectedOptions.Count == 0)) + _modMerger.SplitIntoMod(_newModName); + + if (_modMerger.Error != null) + { + ImGui.Separator(); + ImGui.Dummy(Vector2.One); + using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); + ImGuiUtil.TextWrapped(_modMerger.Error.ToString()); + } + } + + private void DrawCombo() + { + _modCombo.Draw("##ModSelection", _modCombo.CurrentSelection?.Name.Text ?? string.Empty, string.Empty, + 200 * ImGuiHelpers.GlobalScale, ImGui.GetTextLineHeight()); + _modMerger.MergeToMod = _modCombo.CurrentSelection; + } +}