mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 10:17:22 +01:00
Add start of mod merger.
This commit is contained in:
parent
8e5ed60c79
commit
d403f44256
12 changed files with 538 additions and 20 deletions
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -8,21 +8,18 @@ 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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
361
Penumbra/Mods/Editor/ModMerger.cs
Normal file
361
Penumbra/Mods/Editor/ModMerger.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
104
Penumbra/UI/AdvancedWindow/ModMergeTab.cs
Normal file
104
Penumbra/UI/AdvancedWindow/ModMergeTab.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue