Add automatic reduplication for ui files in pmps, test.

This commit is contained in:
Ottermandias 2024-07-12 17:37:19 +02:00
parent 6beb2416dc
commit 40be298d67
6 changed files with 145 additions and 39 deletions

View file

@ -96,6 +96,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService
public DoubleModifier DeleteModModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift); public DoubleModifier DeleteModModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift);
public bool PrintSuccessfulCommandsToChat { get; set; } = true; public bool PrintSuccessfulCommandsToChat { get; set; } = true;
public bool AutoDeduplicateOnImport { get; set; } = true; public bool AutoDeduplicateOnImport { get; set; } = true;
public bool AutoReduplicateUiOnImport { get; set; } = true;
public bool UseFileSystemCompression { get; set; } = true; public bool UseFileSystemCompression { get; set; } = true;
public bool EnableHttpApi { get; set; } = true; public bool EnableHttpApi { get; set; } = true;

View file

@ -1,9 +1,7 @@
using System.IO;
using Dalamud.Utility; using Dalamud.Utility;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using OtterGui.Filesystem; using OtterGui.Filesystem;
using Penumbra.GameData.Files;
using Penumbra.Import.Structs; using Penumbra.Import.Structs;
using Penumbra.Mods; using Penumbra.Mods;
using SharpCompress.Archives; using SharpCompress.Archives;
@ -11,7 +9,6 @@ using SharpCompress.Archives.Rar;
using SharpCompress.Archives.SevenZip; using SharpCompress.Archives.SevenZip;
using SharpCompress.Common; using SharpCompress.Common;
using SharpCompress.Readers; using SharpCompress.Readers;
using static FFXIVClientStructs.FFXIV.Client.UI.Misc.ConfigModule;
using ZipArchive = SharpCompress.Archives.Zip.ZipArchive; using ZipArchive = SharpCompress.Archives.Zip.ZipArchive;
namespace Penumbra.Import; namespace Penumbra.Import;
@ -114,6 +111,7 @@ public partial class TexToolsImporter
_currentModDirectory.Refresh(); _currentModDirectory.Refresh();
_modManager.Creator.SplitMultiGroups(_currentModDirectory); _modManager.Creator.SplitMultiGroups(_currentModDirectory);
_editor.ModNormalizer.NormalizeUi(_currentModDirectory);
return _currentModDirectory; return _currentModDirectory;
} }

View file

@ -20,59 +20,61 @@ public partial class TexToolsImporter
private string _currentOptionName = string.Empty; private string _currentOptionName = string.Empty;
private string _currentFileName = string.Empty; private string _currentFileName = string.Empty;
public void DrawProgressInfo(Vector2 size) public bool DrawProgressInfo(Vector2 size)
{ {
if (_modPackCount == 0) if (_modPackCount == 0)
{ {
ImGuiUtil.Center("Nothing to extract."); ImGuiUtil.Center("Nothing to extract.");
return false;
} }
else if (_modPackCount == _currentModPackIdx)
{ if (_modPackCount == _currentModPackIdx)
DrawEndState(); return DrawEndState();
}
ImGui.NewLine();
var percentage = (float)_currentModPackIdx / _modPackCount;
ImGui.ProgressBar(percentage, size, $"Mod {_currentModPackIdx + 1} / {_modPackCount}");
ImGui.NewLine();
if (State == ImporterState.DeduplicatingFiles)
ImGui.TextUnformatted($"Deduplicating {_currentModName}...");
else else
ImGui.TextUnformatted($"Extracting {_currentModName}...");
if (_currentNumOptions > 1)
{ {
ImGui.NewLine(); ImGui.NewLine();
var percentage = (float)_currentModPackIdx / _modPackCount;
ImGui.ProgressBar(percentage, size, $"Mod {_currentModPackIdx + 1} / {_modPackCount}");
ImGui.NewLine(); ImGui.NewLine();
if (State == ImporterState.DeduplicatingFiles) percentage = _currentNumOptions == 0 ? 1f : _currentOptionIdx / (float)_currentNumOptions;
ImGui.TextUnformatted($"Deduplicating {_currentModName}..."); ImGui.ProgressBar(percentage, size, $"Option {_currentOptionIdx + 1} / {_currentNumOptions}");
else
ImGui.TextUnformatted($"Extracting {_currentModName}...");
if (_currentNumOptions > 1)
{
ImGui.NewLine();
ImGui.NewLine();
percentage = _currentNumOptions == 0 ? 1f : _currentOptionIdx / (float)_currentNumOptions;
ImGui.ProgressBar(percentage, size, $"Option {_currentOptionIdx + 1} / {_currentNumOptions}");
ImGui.NewLine();
if (State != ImporterState.DeduplicatingFiles)
ImGui.TextUnformatted(
$"Extracting option {(_currentGroupName.Length == 0 ? string.Empty : $"{_currentGroupName} - ")}{_currentOptionName}...");
}
ImGui.NewLine();
ImGui.NewLine();
percentage = _currentNumFiles == 0 ? 1f : _currentFileIdx / (float)_currentNumFiles;
ImGui.ProgressBar(percentage, size, $"File {_currentFileIdx + 1} / {_currentNumFiles}");
ImGui.NewLine(); ImGui.NewLine();
if (State != ImporterState.DeduplicatingFiles) if (State != ImporterState.DeduplicatingFiles)
ImGui.TextUnformatted($"Extracting file {_currentFileName}..."); ImGui.TextUnformatted(
$"Extracting option {(_currentGroupName.Length == 0 ? string.Empty : $"{_currentGroupName} - ")}{_currentOptionName}...");
} }
ImGui.NewLine();
ImGui.NewLine();
percentage = _currentNumFiles == 0 ? 1f : _currentFileIdx / (float)_currentNumFiles;
ImGui.ProgressBar(percentage, size, $"File {_currentFileIdx + 1} / {_currentNumFiles}");
ImGui.NewLine();
if (State != ImporterState.DeduplicatingFiles)
ImGui.TextUnformatted($"Extracting file {_currentFileName}...");
return false;
} }
private void DrawEndState() private bool DrawEndState()
{ {
var success = ExtractedMods.Count(t => t.Error == null); var success = ExtractedMods.Count(t => t.Error == null);
if (ImGui.IsKeyPressed(ImGuiKey.Escape))
return true;
ImGui.TextUnformatted($"Successfully extracted {success} / {ExtractedMods.Count} files."); ImGui.TextUnformatted($"Successfully extracted {success} / {ExtractedMods.Count} files.");
ImGui.NewLine(); ImGui.NewLine();
using var table = ImRaii.Table("##files", 2); using var table = ImRaii.Table("##files", 2);
if (!table) if (!table)
return; return false;
foreach (var (file, dir, ex) in ExtractedMods) foreach (var (file, dir, ex) in ExtractedMods)
{ {
@ -91,6 +93,8 @@ public partial class TexToolsImporter
ImGuiUtil.HoverTooltip(ex.ToString()); ImGuiUtil.HoverTooltip(ex.ToString());
} }
} }
return false;
} }
public bool DrawCancelButton(Vector2 size) public bool DrawCancelButton(Vector2 size)

View file

@ -3,13 +3,15 @@ using OtterGui;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Services; using OtterGui.Services;
using OtterGui.Tasks; using OtterGui.Tasks;
using Penumbra.Mods.Groups;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.Mods.SubMods; using Penumbra.Mods.SubMods;
using Penumbra.Services;
using Penumbra.String.Classes; using Penumbra.String.Classes;
namespace Penumbra.Mods.Editor; namespace Penumbra.Mods.Editor;
public class ModNormalizer(ModManager _modManager, Configuration _config) : IService public class ModNormalizer(ModManager modManager, Configuration config, SaveService saveService) : IService
{ {
private readonly List<List<Dictionary<Utf8GamePath, FullPath>>> _redirections = []; private readonly List<List<Dictionary<Utf8GamePath, FullPath>>> _redirections = [];
@ -39,6 +41,103 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) : ISer
Worker = TrackedTask.Run(NormalizeSync); Worker = TrackedTask.Run(NormalizeSync);
} }
public void NormalizeUi(DirectoryInfo modDirectory)
{
if (!config.AutoReduplicateUiOnImport)
return;
if (modManager.Creator.LoadMod(modDirectory, false) is not { } mod)
return;
Dictionary<FullPath, List<(IModDataContainer, Utf8GamePath)>> paths = [];
Dictionary<IModDataContainer, string> containers = [];
foreach (var container in mod.AllDataContainers)
{
foreach (var (gamePath, path) in container.Files)
{
if (!gamePath.Path.StartsWith("ui/"u8))
continue;
if (!paths.TryGetValue(path, out var list))
{
list = [];
paths.Add(path, list);
}
list.Add((container, gamePath));
containers.TryAdd(container, string.Empty);
}
}
foreach (var container in containers.Keys.ToList())
{
if (container.Group == null)
containers[container] = mod.ModPath.FullName;
else
{
var groupDir = ModCreator.NewOptionDirectory(mod.ModPath, container.Group.Name, config.ReplaceNonAsciiOnImport);
var optionDir = ModCreator.NewOptionDirectory(groupDir, container.GetName(), config.ReplaceNonAsciiOnImport);
containers[container] = optionDir.FullName;
}
}
var anyChanges = 0;
var modRootLength = mod.ModPath.FullName.Length + 1;
foreach (var (file, gamePaths) in paths)
{
if (gamePaths.Count < 2)
continue;
var keptPath = false;
foreach (var (container, gamePath) in gamePaths)
{
var directory = containers[container];
var relPath = new Utf8RelPath(gamePath).ToString();
var newFilePath = Path.Combine(directory, relPath);
if (newFilePath == file.FullName)
{
Penumbra.Log.Verbose($"[UIReduplication] Kept {file.FullName[modRootLength..]} because new path was identical.");
keptPath = true;
continue;
}
try
{
Directory.CreateDirectory(Path.GetDirectoryName(newFilePath)!);
File.Copy(file.FullName, newFilePath, false);
Penumbra.Log.Verbose($"[UIReduplication] Copied {file.FullName[modRootLength..]} to {newFilePath[modRootLength..]}.");
container.Files[gamePath] = new FullPath(newFilePath);
++anyChanges;
}
catch (Exception ex)
{
Penumbra.Log.Error(
$"[UIReduplication] Failed to copy {file.FullName[modRootLength..]} to {newFilePath[modRootLength..]}:\n{ex}");
}
}
if (keptPath)
continue;
try
{
File.Delete(file.FullName);
Penumbra.Log.Verbose($"[UIReduplication] Deleted {file.FullName[modRootLength..]} because no new path matched.");
}
catch (Exception ex)
{
Penumbra.Log.Error($"[UIReduplication] Failed to delete {file.FullName[modRootLength..]}:\n{ex}");
}
}
if (anyChanges == 0)
return;
saveService.Save(SaveType.ImmediateSync, new ModSaveGroup(mod.Default, config.ReplaceNonAsciiOnImport));
saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport);
Penumbra.Log.Information($"[UIReduplication] Saved groups after {anyChanges} changes.");
}
private void NormalizeSync() private void NormalizeSync()
{ {
try try
@ -168,7 +267,7 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) : ISer
// Normalize all other options. // Normalize all other options.
foreach (var (group, groupIdx) in Mod.Groups.WithIndex()) foreach (var (group, groupIdx) in Mod.Groups.WithIndex())
{ {
var groupDir = ModCreator.CreateModFolder(directory, group.Name, _config.ReplaceNonAsciiOnImport, true); var groupDir = ModCreator.CreateModFolder(directory, group.Name, config.ReplaceNonAsciiOnImport, true);
_redirections[groupIdx + 1].EnsureCapacity(group.DataContainers.Count); _redirections[groupIdx + 1].EnsureCapacity(group.DataContainers.Count);
for (var i = _redirections[groupIdx + 1].Count; i < group.DataContainers.Count; ++i) for (var i = _redirections[groupIdx + 1].Count; i < group.DataContainers.Count; ++i)
_redirections[groupIdx + 1].Add([]); _redirections[groupIdx + 1].Add([]);
@ -188,7 +287,7 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) : ISer
void HandleSubMod(DirectoryInfo groupDir, IModDataContainer option, Dictionary<Utf8GamePath, FullPath> newDict) void HandleSubMod(DirectoryInfo groupDir, IModDataContainer option, Dictionary<Utf8GamePath, FullPath> newDict)
{ {
var name = option.GetName(); var name = option.GetName();
var optionDir = ModCreator.CreateModFolder(groupDir, name, _config.ReplaceNonAsciiOnImport, true); var optionDir = ModCreator.CreateModFolder(groupDir, name, config.ReplaceNonAsciiOnImport, true);
newDict.Clear(); newDict.Clear();
newDict.EnsureCapacity(option.Files.Count); newDict.EnsureCapacity(option.Files.Count);
@ -277,10 +376,10 @@ public class ModNormalizer(ModManager _modManager, Configuration _config) : ISer
private void ApplyRedirections() private void ApplyRedirections()
{ {
_modManager.OptionEditor.SetFiles(Mod.Default, _redirections[0][0]); modManager.OptionEditor.SetFiles(Mod.Default, _redirections[0][0]);
foreach (var (group, groupIdx) in Mod.Groups.WithIndex()) foreach (var (group, groupIdx) in Mod.Groups.WithIndex())
foreach (var (container, containerIdx) in group.DataContainers.WithIndex()) foreach (var (container, containerIdx) in group.DataContainers.WithIndex())
_modManager.OptionEditor.SetFiles(container, _redirections[groupIdx + 1][containerIdx]); modManager.OptionEditor.SetFiles(container, _redirections[groupIdx + 1][containerIdx]);
++Step; ++Step;
} }

View file

@ -213,6 +213,7 @@ public class Penumbra : IDalamudPlugin
sb.Append( sb.Append(
$"> **`Free Drive Space: `** {(drive != null ? Functions.HumanReadableSize(drive.AvailableFreeSpace) : "Unknown")}\n"); $"> **`Free Drive Space: `** {(drive != null ? Functions.HumanReadableSize(drive.AvailableFreeSpace) : "Unknown")}\n");
sb.Append($"> **`Auto-Deduplication: `** {_config.AutoDeduplicateOnImport}\n"); sb.Append($"> **`Auto-Deduplication: `** {_config.AutoDeduplicateOnImport}\n");
sb.Append($"> **`Auto-UI-Reduplication: `** {_config.AutoReduplicateUiOnImport}\n");
sb.Append($"> **`Debug Mode: `** {_config.DebugMode}\n"); sb.Append($"> **`Debug Mode: `** {_config.DebugMode}\n");
sb.Append( sb.Append(
$"> **`Synchronous Load (Dalamud): `** {(_services.GetService<DalamudConfigService>().GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool v) ? v.ToString() : "Unknown")}\n"); $"> **`Synchronous Load (Dalamud): `** {(_services.GetService<DalamudConfigService>().GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool v) ? v.ToString() : "Unknown")}\n");

View file

@ -757,6 +757,9 @@ public class SettingsTab : ITab, IUiService
Checkbox("Auto Deduplicate on Import", Checkbox("Auto Deduplicate on Import",
"Automatically deduplicate mod files on import. This will make mod file sizes smaller, but deletes (binary identical) files.", "Automatically deduplicate mod files on import. This will make mod file sizes smaller, but deletes (binary identical) files.",
_config.AutoDeduplicateOnImport, v => _config.AutoDeduplicateOnImport = v); _config.AutoDeduplicateOnImport, v => _config.AutoDeduplicateOnImport = v);
Checkbox("Auto Reduplicate UI Files on PMP Import",
"Automatically reduplicate and normalize UI-specific files on import from PMP files. This is STRONGLY recommended because deduplicated UI files crash the game.",
_config.AutoReduplicateUiOnImport, v => _config.AutoReduplicateUiOnImport = v);
DrawCompressionBox(); DrawCompressionBox();
Checkbox("Keep Default Metadata Changes on Import", Checkbox("Keep Default Metadata Changes on Import",
"Normally, metadata changes that equal their default values, which are sometimes exported by TexTools, are discarded. " "Normally, metadata changes that equal their default values, which are sometimes exported by TexTools, are discarded. "