Add start of mod merger.

This commit is contained in:
Ottermandias 2023-05-05 16:19:08 +02:00
parent 8e5ed60c79
commit d403f44256
12 changed files with 538 additions and 20 deletions

View file

@ -37,13 +37,14 @@ public sealed class ModPathChanged : EventWrapper<Action<ModPathChangeType, Mod,
/// <seealso cref="Mods.Manager.ModManager.OnModPathChange"/> /// <seealso cref="Mods.Manager.ModManager.OnModPathChange"/>
ModManager = 0, ModManager = 0,
/// <seealso cref="Mods.Editor.ModMerger.OnModPathChange"/>
ModMerger = 0,
/// <seealso cref="Collections.Manager.CollectionStorage.OnModPathChange"/> /// <seealso cref="Collections.Manager.CollectionStorage.OnModPathChange"/>
CollectionStorage = 10, CollectionStorage = 10,
/// <seealso cref="Collections.Cache.CollectionCacheManager.OnModChangeRemoval"/> /// <seealso cref="Collections.Cache.CollectionCacheManager.OnModChangeRemoval"/>
CollectionCacheManagerRemoval = 100, CollectionCacheManagerRemoval = 100,
} }
public ModPathChanged() public ModPathChanged()
: base(nameof(ModPathChanged)) : base(nameof(ModPathChanged))

View file

@ -8,22 +8,19 @@ using System.Threading.Tasks;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.String.Classes; using Penumbra.String.Classes;
using Penumbra.Util;
namespace Penumbra.Mods; namespace Penumbra.Mods.Editor;
public class DuplicateManager public class DuplicateManager
{ {
private readonly SaveService _saveService; private readonly SaveService _saveService;
private readonly ModManager _modManager; private readonly ModManager _modManager;
private readonly SHA256 _hasher = SHA256.Create(); private readonly SHA256 _hasher = SHA256.Create();
private readonly ModFileCollection _files;
private readonly List<(FullPath[] Paths, long Size, byte[] Hash)> _duplicates = new(); 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; _saveService = saveService;
} }
@ -43,7 +40,7 @@ public class DuplicateManager
Task.Run(() => CheckDuplicates(filesTmp)); 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) if (!Finished || _duplicates.Count == 0)
return; return;
@ -60,7 +57,7 @@ public class DuplicateManager
_duplicates.Clear(); _duplicates.Clear();
DeleteEmptyDirectories(mod.ModPath); DeleteEmptyDirectories(mod.ModPath);
_files.UpdateAll(mod, option); files.UpdateAll(mod, option);
} }
public void Clear() public void Clear()
@ -248,9 +245,10 @@ public class DuplicateManager
_modManager.Creator.ReloadMod(mod, true, out _); _modManager.Creator.ReloadMod(mod, true, out _);
Finished = false; Finished = false;
_files.UpdateAll(mod, mod.Default); var files = new ModFileCollection();
CheckDuplicates(_files.Available.OrderByDescending(f => f.FileSize).ToArray()); files.UpdateAll(mod, mod.Default);
DeleteDuplicates(mod, mod.Default, false); CheckDuplicates(files.Available.OrderByDescending(f => f.FileSize).ToArray());
DeleteDuplicates(files, mod, mod.Default, false);
} }
catch (Exception e) catch (Exception e)
{ {

View file

@ -7,6 +7,7 @@ using System.Text.RegularExpressions;
using OtterGui; using OtterGui;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
using Penumbra.Mods.Editor;
namespace Penumbra.Mods; namespace Penumbra.Mods;

View file

@ -1,6 +1,7 @@
using System; using System;
using System.IO; using System.IO;
using OtterGui; using OtterGui;
using Penumbra.Mods.Editor;
namespace Penumbra.Mods; namespace Penumbra.Mods;

View file

@ -3,11 +3,10 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using Microsoft.Win32;
using OtterGui; using OtterGui;
using Penumbra.String.Classes; using Penumbra.String.Classes;
namespace Penumbra.Mods; namespace Penumbra.Mods.Editor;
public class ModFileCollection : IDisposable public class ModFileCollection : IDisposable
{ {

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Penumbra.Mods.Editor;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.String.Classes; using Penumbra.String.Classes;

View file

@ -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<string, string> _fileToFile = new();
private readonly HashSet<string> _createdDirectories = new();
public readonly HashSet<SubMod> 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<Mod> 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<SubMod> 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<Utf8GamePath, FullPath> CopySubModFiles(SubMod option, DirectoryInfo newMod)
{
var ret = new Dictionary<Utf8GamePath, FullPath>(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;
}
}
}

View file

@ -87,7 +87,7 @@ public class ModOptionEditor
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1);
} }
/// <summary> Add a new mod, empty option group of the given type and name. </summary> /// <summary> Add a new, empty option group of the given type and name. </summary>
public void AddModGroup(Mod mod, GroupType type, string newName) public void AddModGroup(Mod mod, GroupType type, string newName)
{ {
if (!VerifyFileName(mod, null, newName, true)) 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); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod.Groups.Count - 1, -1, -1);
} }
/// <summary> Add a new mod, empty option group of the given type and name if it does not exist already. </summary>
public (IModGroup, int, bool) FindOrAddModGroup(Mod mod, GroupType type, string newName)
{
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);
}
/// <summary> Delete a given option group. Fires an event to prepare before actually deleting. </summary> /// <summary> Delete a given option group. Fires an event to prepare before actually deleting. </summary>
public void DeleteModGroup(Mod mod, int groupIdx) 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); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1);
} }
/// <summary> Add a new empty option of the given name for the given group if it does not exist already. </summary>
public (SubMod, bool) FindOrAddOption(Mod mod, int groupIdx, string newName)
{
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);
}
/// <summary> Add an existing option to a given group with a given priority. </summary> /// <summary> Add an existing option to a given group with a given priority. </summary>
public void AddOption(Mod mod, int groupIdx, ISubMod option, int priority = 0) public void AddOption(Mod mod, int groupIdx, ISubMod option, int priority = 0)
{ {

View file

@ -2,9 +2,24 @@ using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using OtterGui.Classes;
using OtterGui.Widgets;
namespace Penumbra.Mods.Manager; namespace Penumbra.Mods.Manager;
public class ModCombo : FilterComboCache<Mod>
{
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<IReadOnlyList<Mod>> generator)
: base(generator)
{ }
}
public class ModStorage : IReadOnlyList<Mod> public class ModStorage : IReadOnlyList<Mod>
{ {
/// <summary> The actual list of mods. </summary> /// <summary> The actual list of mods. </summary>

View file

@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using Dalamud.Plugin; using Dalamud.Plugin;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using OtterGui.Classes; using OtterGui.Classes;
@ -13,6 +14,7 @@ using Penumbra.Interop.ResourceTree;
using Penumbra.Interop.Services; using Penumbra.Interop.Services;
using Penumbra.Meta; using Penumbra.Meta;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Mods.Editor;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.UI; using Penumbra.UI;
using Penumbra.UI.AdvancedWindow; using Penumbra.UI.AdvancedWindow;
@ -158,7 +160,8 @@ public static class ServiceManager
.AddSingleton<ResourceTab>() .AddSingleton<ResourceTab>()
.AddSingleton<ConfigTabBar>() .AddSingleton<ConfigTabBar>()
.AddSingleton<ResourceWatcher>() .AddSingleton<ResourceWatcher>()
.AddSingleton<ItemSwapTab>(); .AddSingleton<ItemSwapTab>()
.AddSingleton<ModMergeTab>();
private static IServiceCollection AddModEditor(this IServiceCollection services) private static IServiceCollection AddModEditor(this IServiceCollection services)
=> services.AddSingleton<ModFileCollection>() => services.AddSingleton<ModFileCollection>()
@ -168,6 +171,7 @@ public static class ServiceManager
.AddSingleton<ModMetaEditor>() .AddSingleton<ModMetaEditor>()
.AddSingleton<ModSwapEditor>() .AddSingleton<ModSwapEditor>()
.AddSingleton<ModNormalizer>() .AddSingleton<ModNormalizer>()
.AddSingleton<ModMerger>()
.AddSingleton<ModEditor>(); .AddSingleton<ModEditor>();
private static IServiceCollection AddApi(this IServiceCollection services) private static IServiceCollection AddApi(this IServiceCollection services)

View file

@ -35,6 +35,7 @@ public partial class ModEditWindow : Window, IDisposable
private readonly MetaFileManager _metaFileManager; private readonly MetaFileManager _metaFileManager;
private readonly ActiveCollections _activeCollections; private readonly ActiveCollections _activeCollections;
private readonly StainService _stainService; private readonly StainService _stainService;
private readonly ModMergeTab _modMergeTab;
private Mod? _mod; private Mod? _mod;
private Vector2 _iconSize = Vector2.Zero; private Vector2 _iconSize = Vector2.Zero;
@ -144,6 +145,7 @@ public partial class ModEditWindow : Window, IDisposable
DrawFileTab(); DrawFileTab();
DrawMetaTab(); DrawMetaTab();
DrawSwapTab(); DrawSwapTab();
_modMergeTab.Draw();
DrawMissingFilesTab(); DrawMissingFilesTab();
DrawDuplicatesTab(); DrawDuplicatesTab();
DrawQuickImportTab(); DrawQuickImportTab();
@ -311,7 +313,7 @@ public partial class ModEditWindow : Window, IDisposable
} }
if (ImGui.Button("Delete and Redirect Duplicates")) 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) if (_editor.Duplicates.SavedSpace > 0)
{ {
@ -419,7 +421,8 @@ public partial class ModEditWindow : Window, IDisposable
if (otherSwaps > 0) if (otherSwaps > 0)
{ {
ImGui.SameLine(); 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); 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, public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, DataManager gameData,
Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager, 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) : base(WindowBaseLabel)
{ {
_performance = performance; _performance = performance;
@ -520,6 +523,7 @@ public partial class ModEditWindow : Window, IDisposable
_stainService = stainService; _stainService = stainService;
_activeCollections = activeCollections; _activeCollections = activeCollections;
_dalamud = dalamud; _dalamud = dalamud;
_modMergeTab = modMergeTab;
_fileDialog = fileDialog; _fileDialog = fileDialog;
_materialTab = new FileEditor<MtrlTab>(this, gameData, config, _fileDialog, "Materials", ".mtrl", _materialTab = new FileEditor<MtrlTab>(this, gameData, config, _fileDialog, "Materials", ".mtrl",
() => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, () => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty,

View file

@ -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;
}
}