From 577669b21f923f89e8a59712b101c8ca209f074c Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sat, 1 Apr 2023 14:24:12 +0200 Subject: [PATCH] Some mod movement. --- Penumbra/Api/PenumbraApi.cs | 2 +- Penumbra/Configuration.cs | 1 + Penumbra/Import/TexToolsImporter.Archives.cs | 8 +- Penumbra/Import/TexToolsImporter.ModPack.cs | 22 +- Penumbra/Mods/Editor/ModFileEditor.cs | 1 + Penumbra/Mods/Editor/ModNormalizer.cs | 4 +- Penumbra/Mods/Manager/ModCacheManager.cs | 32 +- Penumbra/Mods/Manager/ModDataEditor.cs | 62 +-- Penumbra/Mods/{ => Manager}/ModFileSystem.cs | 3 +- Penumbra/Mods/Manager/ModManager.cs | 8 +- Penumbra/Mods/Manager/ModMigration.cs | 244 +++++++++ Penumbra/Mods/Mod.Creator.cs | 511 +++++++++--------- Penumbra/Mods/Mod.Files.cs | 4 +- Penumbra/Mods/Mod.LocalData.cs | 77 --- Penumbra/Mods/Mod.Meta.Migration.cs | 246 --------- Penumbra/Mods/Mod.Meta.cs | 59 -- Penumbra/Mods/Mod.cs | 35 ++ Penumbra/Mods/ModCache.cs | 2 +- Penumbra/Mods/ModLocalData.cs | 67 +++ Penumbra/Mods/ModMeta.cs | 36 ++ Penumbra/Mods/TemporaryMod.cs | 2 +- Penumbra/Penumbra.cs | 1 - Penumbra/UI/AdvancedWindow/ItemSwapTab.cs | 6 +- .../ModEditWindow.QuickImport.cs | 6 +- Penumbra/UI/ModsTab/ModFileSystemSelector.cs | 4 +- Penumbra/Util/SaveService.cs | 15 +- 26 files changed, 726 insertions(+), 732 deletions(-) rename Penumbra/Mods/{ => Manager}/ModFileSystem.cs (99%) create mode 100644 Penumbra/Mods/Manager/ModMigration.cs delete mode 100644 Penumbra/Mods/Mod.LocalData.cs delete mode 100644 Penumbra/Mods/Mod.Meta.Migration.cs delete mode 100644 Penumbra/Mods/Mod.Meta.cs create mode 100644 Penumbra/Mods/Mod.cs create mode 100644 Penumbra/Mods/ModLocalData.cs create mode 100644 Penumbra/Mods/ModMeta.cs diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 4e49ab0a..56251935 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -828,7 +828,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi public PenumbraApiEc CreateNamedTemporaryCollection(string name) { CheckInitialized(); - if (name.Length == 0 || Mod.Creator.ReplaceBadXivSymbols(name) != name) + if (name.Length == 0 || ModCreator.ReplaceBadXivSymbols(name) != name) return PenumbraApiEc.InvalidArgument; return _tempCollections.CreateTemporaryCollection(name).Length > 0 diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index efe21ffe..89b29978 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -11,6 +11,7 @@ using OtterGui.Widgets; using Penumbra.GameData.Enums; using Penumbra.Import.Structs; using Penumbra.Mods; +using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.UI; using Penumbra.UI.Classes; diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs index c986cc78..6234e9ca 100644 --- a/Penumbra/Import/TexToolsImporter.Archives.cs +++ b/Penumbra/Import/TexToolsImporter.Archives.cs @@ -45,7 +45,7 @@ public partial class TexToolsImporter }; Penumbra.Log.Information( $" -> Importing {archive.Type} Archive." ); - _currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, Path.GetRandomFileName() ); + _currentModDirectory = ModCreator.CreateModFolder( _baseDirectory, Path.GetRandomFileName() ); var options = new ExtractionOptions() { ExtractFullPath = true, @@ -100,18 +100,18 @@ public partial class TexToolsImporter // Use either the top-level directory as the mods base name, or the (fixed for path) name in the json. if( leadDir ) { - _currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, baseName, false ); + _currentModDirectory = ModCreator.CreateModFolder( _baseDirectory, baseName, false ); Directory.Move( Path.Combine( oldName, baseName ), _currentModDirectory.FullName ); Directory.Delete( oldName ); } else { - _currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, name, false ); + _currentModDirectory = ModCreator.CreateModFolder( _baseDirectory, name, false ); Directory.Move( oldName, _currentModDirectory.FullName ); } _currentModDirectory.Refresh(); - Mod.Creator.SplitMultiGroups( _currentModDirectory ); + ModCreator.SplitMultiGroups( _currentModDirectory ); return _currentModDirectory; } diff --git a/Penumbra/Import/TexToolsImporter.ModPack.cs b/Penumbra/Import/TexToolsImporter.ModPack.cs index a8cb6608..5c06dcdc 100644 --- a/Penumbra/Import/TexToolsImporter.ModPack.cs +++ b/Penumbra/Import/TexToolsImporter.ModPack.cs @@ -33,14 +33,14 @@ public partial class TexToolsImporter var modList = modListRaw.Select( m => JsonConvert.DeserializeObject< SimpleMod >( m, JsonSettings )! ).ToList(); - _currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, Path.GetFileNameWithoutExtension( modPackFile.Name ) ); + _currentModDirectory = ModCreator.CreateModFolder( _baseDirectory, Path.GetFileNameWithoutExtension( modPackFile.Name ) ); // Create a new ModMeta from the TTMP mod list info _modManager.DataEditor.CreateMeta( _currentModDirectory, _currentModName, DefaultTexToolsData.Author, DefaultTexToolsData.Description, null, null ); // Open the mod data file from the mod pack as a SqPackStream _streamDisposer = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" ); ExtractSimpleModList( _currentModDirectory, modList ); - Mod.Creator.CreateDefaultFiles( _currentModDirectory ); + ModCreator.CreateDefaultFiles( _currentModDirectory ); ResetStreamDisposer(); return _currentModDirectory; } @@ -89,7 +89,7 @@ public partial class TexToolsImporter _currentOptionName = DefaultTexToolsData.DefaultOption; Penumbra.Log.Information( " -> Importing Simple V2 ModPack" ); - _currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, _currentModName ); + _currentModDirectory = ModCreator.CreateModFolder( _baseDirectory, _currentModName ); _modManager.DataEditor.CreateMeta( _currentModDirectory, _currentModName, modList.Author, string.IsNullOrEmpty( modList.Description ) ? "Mod imported from TexTools mod pack" : modList.Description, modList.Version, modList.Url ); @@ -97,7 +97,7 @@ public partial class TexToolsImporter // Open the mod data file from the mod pack as a SqPackStream _streamDisposer = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" ); ExtractSimpleModList( _currentModDirectory, modList.SimpleModsList ); - Mod.Creator.CreateDefaultFiles( _currentModDirectory ); + ModCreator.CreateDefaultFiles( _currentModDirectory ); ResetStreamDisposer(); return _currentModDirectory; } @@ -134,7 +134,7 @@ public partial class TexToolsImporter _currentNumOptions = GetOptionCount( modList ); _currentModName = modList.Name; - _currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, _currentModName ); + _currentModDirectory = ModCreator.CreateModFolder( _baseDirectory, _currentModName ); _modManager.DataEditor.CreateMeta( _currentModDirectory, _currentModName, modList.Author, modList.Description, modList.Version, modList.Url ); if( _currentNumOptions == 0 ) @@ -172,7 +172,7 @@ public partial class TexToolsImporter { var name = numGroups == 1 ? _currentGroupName : $"{_currentGroupName}, Part {groupId + 1}"; options.Clear(); - var groupFolder = Mod.Creator.NewSubFolderName( _currentModDirectory, name ) + var groupFolder = ModCreator.NewSubFolderName( _currentModDirectory, name ) ?? new DirectoryInfo( Path.Combine( _currentModDirectory.FullName, numGroups == 1 ? $"Group {groupPriority + 1}" : $"Group {groupPriority + 1}, Part {groupId + 1}" ) ); @@ -182,10 +182,10 @@ public partial class TexToolsImporter var option = allOptions[ i + optionIdx ]; _token.ThrowIfCancellationRequested(); _currentOptionName = option.Name; - var optionFolder = Mod.Creator.NewSubFolderName( groupFolder, option.Name ) + var optionFolder = ModCreator.NewSubFolderName( groupFolder, option.Name ) ?? new DirectoryInfo( Path.Combine( groupFolder.FullName, $"Option {i + optionIdx + 1}" ) ); ExtractSimpleModList( optionFolder, option.ModsJsons ); - options.Add( Mod.Creator.CreateSubMod( _currentModDirectory, optionFolder, option ) ); + options.Add( ModCreator.CreateSubMod( _currentModDirectory, optionFolder, option ) ); if( option.IsChecked ) { defaultSettings = group.SelectionType == GroupType.Multi @@ -206,12 +206,12 @@ public partial class TexToolsImporter if( empty != null ) { _currentOptionName = empty.Name; - options.Insert( 0, Mod.Creator.CreateEmptySubMod( empty.Name ) ); + options.Insert( 0, ModCreator.CreateEmptySubMod( empty.Name ) ); defaultSettings = defaultSettings == null ? 0 : defaultSettings.Value + 1; } } - Mod.Creator.CreateOptionGroup( _currentModDirectory, group.SelectionType, name, groupPriority, groupPriority, + ModCreator.CreateOptionGroup( _currentModDirectory, group.SelectionType, name, groupPriority, groupPriority, defaultSettings ?? 0, group.Description, options ); ++groupPriority; } @@ -219,7 +219,7 @@ public partial class TexToolsImporter } ResetStreamDisposer(); - Mod.Creator.CreateDefaultFiles( _currentModDirectory ); + ModCreator.CreateDefaultFiles( _currentModDirectory ); return _currentModDirectory; } diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index c9813b33..9e649fbb 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.Manager; using Penumbra.String.Classes; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index 395d71dd..d920adc4 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -181,10 +181,10 @@ public class ModNormalizer for (var i = _redirections[groupIdx + 1].Count; i < group.Count; ++i) _redirections[groupIdx + 1].Add(new Dictionary()); - var groupDir = Mod.Creator.CreateModFolder(directory, group.Name); + var groupDir = ModCreator.CreateModFolder(directory, group.Name); foreach (var option in group.OfType()) { - var optionDir = Mod.Creator.CreateModFolder(groupDir, option.Name); + var optionDir = ModCreator.CreateModFolder(groupDir, option.Name); newDict = _redirections[groupIdx + 1][option.OptionIdx]; newDict.Clear(); diff --git a/Penumbra/Mods/Manager/ModCacheManager.cs b/Penumbra/Mods/Manager/ModCacheManager.cs index 3350119f..85637707 100644 --- a/Penumbra/Mods/Manager/ModCacheManager.cs +++ b/Penumbra/Mods/Manager/ModCacheManager.cs @@ -10,25 +10,25 @@ using Penumbra.GameData.Enums; using Penumbra.Meta.Manipulations; using Penumbra.Services; -namespace Penumbra.Mods; +namespace Penumbra.Mods.Manager; public class ModCacheManager : IDisposable, IReadOnlyList { private readonly CommunicatorService _communicator; - private readonly IdentifierService _identifier; - private readonly IReadOnlyList _modManager; + private readonly IdentifierService _identifier; + private readonly IReadOnlyList _modManager; private readonly List _cache = new(); public ModCacheManager(CommunicatorService communicator, IdentifierService identifier, ModManager modManager) { _communicator = communicator; - _identifier = identifier; - _modManager = modManager; + _identifier = identifier; + _modManager = modManager; - _communicator.ModOptionChanged.Event += OnModOptionChange; - _communicator.ModPathChanged.Event += OnModPathChange; - _communicator.ModDataChanged.Event += OnModDataChange; + _communicator.ModOptionChanged.Event += OnModOptionChange; + _communicator.ModPathChanged.Event += OnModPathChange; + _communicator.ModDataChanged.Event += OnModDataChange; _communicator.ModDiscoveryFinished.Event += OnModDiscoveryFinished; if (!identifier.Valid) identifier.FinishedCreation += OnIdentifierCreation; @@ -51,9 +51,9 @@ public class ModCacheManager : IDisposable, IReadOnlyList public void Dispose() { - _communicator.ModOptionChanged.Event -= OnModOptionChange; - _communicator.ModPathChanged.Event -= OnModPathChange; - _communicator.ModDataChanged.Event -= OnModDataChange; + _communicator.ModOptionChanged.Event -= OnModOptionChange; + _communicator.ModPathChanged.Event -= OnModPathChange; + _communicator.ModDataChanged.Event -= OnModDataChange; _communicator.ModDiscoveryFinished.Event -= OnModDiscoveryFinished; } @@ -232,17 +232,17 @@ public class ModCacheManager : IDisposable, IReadOnlyList private static void UpdateCounts(ModCache cache, Mod mod) { - cache.TotalFileCount = mod.Default.Files.Count; - cache.TotalSwapCount = mod.Default.FileSwaps.Count; + cache.TotalFileCount = mod.Default.Files.Count; + cache.TotalSwapCount = mod.Default.FileSwaps.Count; cache.TotalManipulations = mod.Default.Manipulations.Count; - cache.HasOptions = false; + cache.HasOptions = false; foreach (var group in mod.Groups) { cache.HasOptions |= group.IsOption; foreach (var s in group) { - cache.TotalFileCount += s.Files.Count; - cache.TotalSwapCount += s.FileSwaps.Count; + cache.TotalFileCount += s.Files.Count; + cache.TotalSwapCount += s.FileSwaps.Count; cache.TotalManipulations += s.Manipulations.Count; } } diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index 48f13514..86bd826e 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -7,7 +7,7 @@ using OtterGui.Classes; using Penumbra.Services; using Penumbra.Util; -namespace Penumbra.Mods; +namespace Penumbra.Mods.Manager; [Flags] public enum ModDataChangeType : ushort @@ -25,27 +25,25 @@ public enum ModDataChangeType : ushort Favorite = 0x0200, LocalTags = 0x0400, Note = 0x0800, -} +} public class ModDataEditor { - private readonly FilenameService _filenameService; private readonly SaveService _saveService; private readonly CommunicatorService _communicatorService; - public ModDataEditor(FilenameService filenameService, SaveService saveService, CommunicatorService communicatorService) + public ModDataEditor(SaveService saveService, CommunicatorService communicatorService) { - _filenameService = filenameService; _saveService = saveService; _communicatorService = communicatorService; } public string MetaFile(Mod mod) - => _filenameService.ModMetaPath(mod); + => _saveService.FileNames.ModMetaPath(mod); public string DataFile(Mod mod) - => _filenameService.LocalDataFile(mod); - + => _saveService.FileNames.LocalDataFile(mod); + /// Create the file containing the meta information about a mod from scratch. public void CreateMeta(DirectoryInfo directory, string? name, string? author, string? description, string? version, string? website) @@ -56,12 +54,12 @@ public class ModDataEditor mod.Description = description ?? mod.Description; mod.Version = version ?? mod.Version; mod.Website = website ?? mod.Website; - _saveService.ImmediateSave(new Mod.ModMeta(mod)); + _saveService.ImmediateSave(new ModMeta(mod)); } public ModDataChangeType LoadLocalData(Mod mod) { - var dataFile = _filenameService.LocalDataFile(mod); + var dataFile = _saveService.FileNames.LocalDataFile(mod); var importDate = 0L; var localTags = Enumerable.Empty(); @@ -98,7 +96,7 @@ public class ModDataEditor changes |= ModDataChangeType.ImportDate; } - changes |= mod.UpdateTags(null, localTags); + changes |= ModLocalData.UpdateTags(mod, null, localTags); if (mod.Favorite != favorite) { @@ -113,14 +111,14 @@ public class ModDataEditor } if (save) - _saveService.QueueSave(new Mod.ModData(mod)); + _saveService.QueueSave(new ModLocalData(mod)); return changes; } public ModDataChangeType LoadMeta(Mod mod) { - var metaFile = _filenameService.ModMetaPath(mod); + var metaFile = _saveService.FileNames.ModMetaPath(mod); if (!File.Exists(metaFile)) { Penumbra.Log.Debug($"No mod meta found for {mod.ModPath.Name}."); @@ -137,7 +135,7 @@ public class ModDataEditor var newDescription = json[nameof(Mod.Description)]?.Value() ?? string.Empty; var newVersion = json[nameof(Mod.Version)]?.Value() ?? string.Empty; var newWebsite = json[nameof(Mod.Website)]?.Value() ?? string.Empty; - var newFileVersion = json[nameof(Mod.ModMeta.FileVersion)]?.Value() ?? 0; + var newFileVersion = json[nameof(ModMeta.FileVersion)]?.Value() ?? 0; var importDate = json[nameof(Mod.ImportDate)]?.Value(); var modTags = json[nameof(Mod.ModTags)]?.Values().OfType(); @@ -172,14 +170,12 @@ public class ModDataEditor mod.Website = newWebsite; } - if (newFileVersion != Mod.ModMeta.FileVersion) - { - if (Mod.Migration.Migrate(mod, json, ref newFileVersion)) + if (newFileVersion != ModMeta.FileVersion) + if (ModMigration.Migrate(_saveService, mod, json, ref newFileVersion)) { changes |= ModDataChangeType.Migration; - _saveService.ImmediateSave(new Mod.ModMeta(mod)); + _saveService.ImmediateSave(new ModMeta(mod)); } - } if (importDate != null && mod.ImportDate != importDate.Value) { @@ -187,7 +183,7 @@ public class ModDataEditor changes |= ModDataChangeType.ImportDate; } - changes |= mod.UpdateTags(modTags, null); + changes |= ModLocalData.UpdateTags(mod, modTags, null); return changes; } @@ -205,7 +201,7 @@ public class ModDataEditor var oldName = mod.Name; mod.Name = newName; - _saveService.QueueSave(new Mod.ModMeta(mod)); + _saveService.QueueSave(new ModMeta(mod)); _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Name, mod, oldName.Text); } @@ -215,7 +211,7 @@ public class ModDataEditor return; mod.Author = newAuthor; - _saveService.QueueSave(new Mod.ModMeta(mod)); + _saveService.QueueSave(new ModMeta(mod)); _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Author, mod, null); } @@ -225,7 +221,7 @@ public class ModDataEditor return; mod.Description = newDescription; - _saveService.QueueSave(new Mod.ModMeta(mod)); + _saveService.QueueSave(new ModMeta(mod)); _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Description, mod, null); } @@ -235,7 +231,7 @@ public class ModDataEditor return; mod.Version = newVersion; - _saveService.QueueSave(new Mod.ModMeta(mod)); + _saveService.QueueSave(new ModMeta(mod)); _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Version, mod, null); } @@ -245,7 +241,7 @@ public class ModDataEditor return; mod.Website = newWebsite; - _saveService.QueueSave(new Mod.ModMeta(mod)); + _saveService.QueueSave(new ModMeta(mod)); _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Website, mod, null); } @@ -261,7 +257,7 @@ public class ModDataEditor return; mod.Favorite = state; - _saveService.QueueSave(new Mod.ModData(mod)); + _saveService.QueueSave(new ModLocalData(mod)); ; _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Favorite, mod, null); } @@ -272,7 +268,7 @@ public class ModDataEditor return; mod.Note = newNote; - _saveService.QueueSave(new Mod.ModData(mod)); + _saveService.QueueSave(new ModLocalData(mod)); ; _communicatorService.ModDataChanged.Invoke(ModDataChangeType.Favorite, mod, null); } @@ -287,20 +283,20 @@ public class ModDataEditor ModDataChangeType flags = 0; if (tagIdx == which.Count) { - flags = mod.UpdateTags(local ? null : which.Append(newTag), local ? which.Append(newTag) : null); + flags = ModLocalData.UpdateTags(mod, local ? null : which.Append(newTag), local ? which.Append(newTag) : null); } else { var tmp = which.ToArray(); tmp[tagIdx] = newTag; - flags = mod.UpdateTags(local ? null : tmp, local ? tmp : null); + flags = ModLocalData.UpdateTags(mod, local ? null : tmp, local ? tmp : null); } if (flags.HasFlag(ModDataChangeType.ModTags)) - _saveService.QueueSave(new Mod.ModMeta(mod)); + _saveService.QueueSave(new ModMeta(mod)); if (flags.HasFlag(ModDataChangeType.LocalTags)) - _saveService.QueueSave(new Mod.ModData(mod)); + _saveService.QueueSave(new ModLocalData(mod)); if (flags != 0) _communicatorService.ModDataChanged.Invoke(flags, mod, null); @@ -308,8 +304,8 @@ public class ModDataEditor public void MoveDataFile(DirectoryInfo oldMod, DirectoryInfo newMod) { - var oldFile = _filenameService.LocalDataFile(oldMod.Name); - var newFile = _filenameService.LocalDataFile(newMod.Name); + var oldFile = _saveService.FileNames.LocalDataFile(oldMod.Name); + var newFile = _saveService.FileNames.LocalDataFile(newMod.Name); if (!File.Exists(oldFile)) return; diff --git a/Penumbra/Mods/ModFileSystem.cs b/Penumbra/Mods/Manager/ModFileSystem.cs similarity index 99% rename from Penumbra/Mods/ModFileSystem.cs rename to Penumbra/Mods/Manager/ModFileSystem.cs index 3ee5fa66..7f5d3070 100644 --- a/Penumbra/Mods/ModFileSystem.cs +++ b/Penumbra/Mods/Manager/ModFileSystem.cs @@ -5,11 +5,10 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; using OtterGui.Filesystem; -using Penumbra.Mods.Manager; using Penumbra.Services; using Penumbra.Util; -namespace Penumbra.Mods; +namespace Penumbra.Mods.Manager; public sealed class ModFileSystem : FileSystem, IDisposable, ISavable { diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index 050c4ed1..7acb2417 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Threading.Tasks; using Penumbra.Services; -namespace Penumbra.Mods; +namespace Penumbra.Mods.Manager; /// Describes the state of a potential move-target for a mod. public enum NewDirectoryState @@ -35,7 +35,7 @@ public sealed class ModManager : ModStorage _config = config; _communicator = communicator; DataEditor = dataEditor; - OptionEditor = optionEditor; + OptionEditor = optionEditor; SetBaseDirectory(config.ModDirectory, true); DiscoverMods(); } @@ -73,7 +73,7 @@ public sealed class ModManager : ModStorage if (this.Any(m => m.ModPath.Name == modFolder.Name)) return; - Mod.Creator.SplitMultiGroups(modFolder); + ModCreator.SplitMultiGroups(modFolder); var mod = Mod.LoadMod(this, modFolder, true); if (mod == null) return; @@ -206,7 +206,7 @@ public sealed class ModManager : ModStorage if (oldName == newName) return NewDirectoryState.Identical; - var fixedNewName = Mod.Creator.ReplaceBadXivSymbols(newName); + var fixedNewName = ModCreator.ReplaceBadXivSymbols(newName); if (fixedNewName != newName) return NewDirectoryState.ContainsInvalidSymbols; diff --git a/Penumbra/Mods/Manager/ModMigration.cs b/Penumbra/Mods/Manager/ModMigration.cs new file mode 100644 index 00000000..196c7ed5 --- /dev/null +++ b/Penumbra/Mods/Manager/ModMigration.cs @@ -0,0 +1,244 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OtterGui; +using Penumbra.Api.Enums; +using Penumbra.String.Classes; +using Penumbra.Util; + +namespace Penumbra.Mods.Manager; + +public static partial class ModMigration +{ + [GeneratedRegex(@"group_\d{3}_", RegexOptions.Compiled | RegexOptions.NonBacktracking | RegexOptions.ExplicitCapture)] + private static partial Regex GroupRegex(); + + [GeneratedRegex("^group_", RegexOptions.Compiled)] + private static partial Regex GroupStartRegex(); + + public static bool Migrate(SaveService saveService, Mod mod, JObject json, ref uint fileVersion) + => MigrateV0ToV1(saveService, mod, json, ref fileVersion) || MigrateV1ToV2(mod, ref fileVersion) || MigrateV2ToV3(mod, ref fileVersion); + + private static bool MigrateV2ToV3(Mod _, ref uint fileVersion) + { + if (fileVersion > 2) + return false; + + // Remove import time. + fileVersion = 3; + return true; + } + + private static bool MigrateV1ToV2(Mod mod, ref uint fileVersion) + { + if (fileVersion > 1) + return false; + + if (!mod.GroupFiles.All(g => GroupRegex().IsMatch(g.Name))) + foreach (var (group, index) in mod.GroupFiles.WithIndex().ToArray()) + { + var newName = GroupStartRegex().Replace(group.Name, $"group_{index + 1:D3}_"); + try + { + if (newName != group.Name) + group.MoveTo(Path.Combine(group.DirectoryName ?? string.Empty, newName), false); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not rename group file {group.Name} to {newName} during migration:\n{e}"); + } + } + + fileVersion = 2; + + return true; + } + + private static bool MigrateV0ToV1(SaveService saveService, Mod mod, JObject json, ref uint fileVersion) + { + if (fileVersion > 0) + return false; + + var swaps = json["FileSwaps"]?.ToObject>() + ?? new Dictionary(); + var groups = json["Groups"]?.ToObject>() ?? new Dictionary(); + var priority = 1; + var seenMetaFiles = new HashSet(); + foreach (var group in groups.Values) + ConvertGroup(mod, group, ref priority, seenMetaFiles); + + foreach (var unusedFile in mod.FindUnusedFiles().Where(f => !seenMetaFiles.Contains(f))) + { + if (unusedFile.ToGamePath(mod.ModPath, out var gamePath) + && !mod._default.FileData.TryAdd(gamePath, unusedFile)) + Penumbra.Log.Error($"Could not add {gamePath} because it already points to {mod._default.FileData[gamePath]}."); + } + + mod._default.FileSwapData.Clear(); + mod._default.FileSwapData.EnsureCapacity(swaps.Count); + foreach (var (gamePath, swapPath) in swaps) + mod._default.FileSwapData.Add(gamePath, swapPath); + + mod._default.IncorporateMetaChanges(mod.ModPath, true); + foreach (var (_, index) in mod.Groups.WithIndex()) + saveService.ImmediateSave(new ModSaveGroup(mod, index)); + + // Delete meta files. + foreach (var file in seenMetaFiles.Where(f => f.Exists)) + { + try + { + File.Delete(file.FullName); + } + catch (Exception e) + { + Penumbra.Log.Warning($"Could not delete meta file {file.FullName} during migration:\n{e}"); + } + } + + // Delete old meta files. + var oldMetaFile = Path.Combine(mod.ModPath.FullName, "metadata_manipulations.json"); + if (File.Exists(oldMetaFile)) + try + { + File.Delete(oldMetaFile); + } + catch (Exception e) + { + Penumbra.Log.Warning($"Could not delete old meta file {oldMetaFile} during migration:\n{e}"); + } + + fileVersion = 1; + saveService.ImmediateSave(new ModSaveGroup(mod, -1)); + + return true; + } + + private static void ConvertGroup(Mod mod, OptionGroupV0 group, ref int priority, HashSet seenMetaFiles) + { + if (group.Options.Count == 0) + return; + + switch (group.SelectionType) + { + case GroupType.Multi: + + var optionPriority = 0; + var newMultiGroup = new MultiModGroup() + { + Name = group.GroupName, + Priority = priority++, + Description = string.Empty, + }; + mod._groups.Add(newMultiGroup); + foreach (var option in group.Options) + newMultiGroup.PrioritizedOptions.Add((SubModFromOption(mod, option, seenMetaFiles), optionPriority++)); + + break; + case GroupType.Single: + if (group.Options.Count == 1) + { + AddFilesToSubMod(mod._default, mod.ModPath, group.Options[0], seenMetaFiles); + return; + } + + var newSingleGroup = new SingleModGroup() + { + Name = group.GroupName, + Priority = priority++, + Description = string.Empty, + }; + mod._groups.Add(newSingleGroup); + foreach (var option in group.Options) + newSingleGroup.OptionData.Add(SubModFromOption(mod, option, seenMetaFiles)); + + break; + } + } + + private static void AddFilesToSubMod(SubMod mod, DirectoryInfo basePath, OptionV0 option, HashSet seenMetaFiles) + { + foreach (var (relPath, gamePaths) in option.OptionFiles) + { + var fullPath = new FullPath(basePath, relPath); + foreach (var gamePath in gamePaths) + mod.FileData.TryAdd(gamePath, fullPath); + + if (fullPath.Extension is ".meta" or ".rgsp") + seenMetaFiles.Add(fullPath); + } + } + + private static SubMod SubModFromOption(Mod mod, OptionV0 option, HashSet seenMetaFiles) + { + var subMod = new SubMod(mod) { Name = option.OptionName }; + AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles); + subMod.IncorporateMetaChanges(mod.ModPath, false); + return subMod; + } + + private struct OptionV0 + { + public string OptionName = string.Empty; + public string OptionDesc = string.Empty; + + [JsonProperty(ItemConverterType = typeof(SingleOrArrayConverter))] + public Dictionary> OptionFiles = new(); + + public OptionV0() + { } + } + + private struct OptionGroupV0 + { + public string GroupName = string.Empty; + + [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] + public GroupType SelectionType = GroupType.Single; + + public List Options = new(); + + public OptionGroupV0() + { } + } + + // Not used anymore, but required for migration. + private class SingleOrArrayConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + => objectType == typeof(HashSet); + + public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + var token = JToken.Load(reader); + + if (token.Type == JTokenType.Array) + return token.ToObject>() ?? new HashSet(); + + var tmp = token.ToObject(); + return tmp != null + ? new HashSet { tmp } + : new HashSet(); + } + + public override bool CanWrite + => true; + + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + writer.WriteStartArray(); + if (value != null) + { + var v = (HashSet)value; + foreach (var val in v) + serializer.Serialize(writer, val?.ToString()); + } + + writer.WriteEndArray(); + } + } +} diff --git a/Penumbra/Mods/Mod.Creator.cs b/Penumbra/Mods/Mod.Creator.cs index 86baa7d9..97b4fe4a 100644 --- a/Penumbra/Mods/Mod.Creator.cs +++ b/Penumbra/Mods/Mod.Creator.cs @@ -13,273 +13,270 @@ using Penumbra.String.Classes; namespace Penumbra.Mods; -public partial class Mod +internal static partial class ModCreator { - internal static partial class Creator + /// + /// Create and return a new directory based on the given directory and name, that is
+ /// - Not Empty.
+ /// - Unique, by appending (digit) for duplicates.
+ /// - Containing no symbols invalid for FFXIV or windows paths.
+ ///
+ /// + /// + /// + /// + /// + public static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName, bool create = true ) { - /// - /// Create and return a new directory based on the given directory and name, that is
- /// - Not Empty.
- /// - Unique, by appending (digit) for duplicates.
- /// - Containing no symbols invalid for FFXIV or windows paths.
- ///
- /// - /// - /// - /// - /// - public static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName, bool create = true ) + var name = modListName; + if( name.Length == 0 ) { - var name = modListName; - if( name.Length == 0 ) - { - name = "_"; - } - - var newModFolderBase = NewOptionDirectory( outDirectory, name ); - var newModFolder = newModFolderBase.FullName.ObtainUniqueFile(); - if( newModFolder.Length == 0 ) - { - throw new IOException( "Could not create mod folder: too many folders of the same name exist." ); - } - - if( create ) - { - Directory.CreateDirectory( newModFolder ); - } - - return new DirectoryInfo( newModFolder ); + name = "_"; } - /// - /// Create the name for a group or option subfolder based on its parent folder and given name. - /// subFolderName should never be empty, and the result is unique and contains no invalid symbols. - /// - public static DirectoryInfo? NewSubFolderName( DirectoryInfo parentFolder, string subFolderName ) + var newModFolderBase = NewOptionDirectory( outDirectory, name ); + var newModFolder = newModFolderBase.FullName.ObtainUniqueFile(); + if( newModFolder.Length == 0 ) { - var newModFolderBase = NewOptionDirectory( parentFolder, subFolderName ); - var newModFolder = newModFolderBase.FullName.ObtainUniqueFile(); - return newModFolder.Length == 0 ? null : new DirectoryInfo( newModFolder ); + throw new IOException( "Could not create mod folder: too many folders of the same name exist." ); } - /// Create a file for an option group from given data. - public static void CreateOptionGroup( DirectoryInfo baseFolder, GroupType type, string name, - int priority, int index, uint defaultSettings, string desc, IEnumerable< ISubMod > subMods ) + if( create ) { - switch( type ) - { - case GroupType.Multi: - { - var group = new MultiModGroup() - { - Name = name, - Description = desc, - Priority = priority, - DefaultSettings = defaultSettings, - }; - group.PrioritizedOptions.AddRange( subMods.OfType< SubMod >().Select( ( s, idx ) => ( s, idx ) ) ); - Penumbra.SaveService.ImmediateSave(new ModSaveGroup(baseFolder, group, index)); - break; - } - case GroupType.Single: - { - var group = new SingleModGroup() - { - Name = name, - Description = desc, - Priority = priority, - DefaultSettings = defaultSettings, - }; - group.OptionData.AddRange( subMods.OfType< SubMod >() ); - Penumbra.SaveService.ImmediateSave(new ModSaveGroup(baseFolder, group, index)); - break; - } - } + Directory.CreateDirectory( newModFolder ); } - /// Create the data for a given sub mod from its data and the folder it is based on. - public static ISubMod CreateSubMod( DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option ) - { - var list = optionFolder.EnumerateFiles( "*.*", SearchOption.AllDirectories ) - .Select( f => ( Utf8GamePath.FromFile( f, optionFolder, out var gamePath, true ), gamePath, new FullPath( f ) ) ) - .Where( t => t.Item1 ); - - var mod = new SubMod( null! ) // Mod is irrelevant here, only used for saving. - { - Name = option.Name, - Description = option.Description, - }; - foreach( var (_, gamePath, file) in list ) - { - mod.FileData.TryAdd( gamePath, file ); - } - - mod.IncorporateMetaChanges( baseFolder, true ); - return mod; - } - - /// Create an empty sub mod for single groups with None options. - internal static ISubMod CreateEmptySubMod( string name ) - => new SubMod( null! ) // Mod is irrelevant here, only used for saving. - { - Name = name, - }; - - /// - /// Create the default data file from all unused files that were not handled before - /// and are used in sub mods. - /// - internal static void CreateDefaultFiles( DirectoryInfo directory ) - { - var mod = new Mod( directory ); - mod.Reload( Penumbra.ModManager, false, out _ ); - foreach( var file in mod.FindUnusedFiles() ) - { - if( Utf8GamePath.FromFile( new FileInfo( file.FullName ), directory, out var gamePath, true ) ) - mod._default.FileData.TryAdd( gamePath, file ); - } - - mod._default.IncorporateMetaChanges( directory, true ); - Penumbra.SaveService.ImmediateSave(new ModSaveGroup(mod, -1)); - } - - /// Return the name of a new valid directory based on the base directory and the given name. - public static DirectoryInfo NewOptionDirectory( DirectoryInfo baseDir, string optionName ) - => new(Path.Combine( baseDir.FullName, ReplaceBadXivSymbols( optionName ) )); - - /// Normalize for nicer names, and remove invalid symbols or invalid paths. - public static string ReplaceBadXivSymbols( string s, string replacement = "_" ) - { - switch( s ) - { - case ".": return replacement; - case "..": return replacement + replacement; - } - - StringBuilder sb = new(s.Length); - foreach( var c in s.Normalize( NormalizationForm.FormKC ) ) - { - if( c.IsInvalidInPath() ) - { - sb.Append( replacement ); - } - else - { - sb.Append( c ); - } - } - - return sb.ToString(); - } - - public static void SplitMultiGroups( DirectoryInfo baseDir ) - { - var mod = new Mod( baseDir ); - - var files = mod.GroupFiles.ToList(); - var idx = 0; - var reorder = false; - foreach( var groupFile in files ) - { - ++idx; - try - { - if( reorder ) - { - var newName = $"{baseDir.FullName}\\group_{idx:D3}{groupFile.Name[ 9.. ]}"; - Penumbra.Log.Debug( $"Moving {groupFile.Name} to {Path.GetFileName( newName )} due to reordering after multi group split." ); - groupFile.MoveTo( newName, false ); - } - } - catch( Exception ex ) - { - throw new Exception( "Could not reorder group file after splitting multi group on .pmp import.", ex ); - } - - try - { - var json = JObject.Parse( File.ReadAllText( groupFile.FullName ) ); - if( json[ nameof( IModGroup.Type ) ]?.ToObject< GroupType >() is not GroupType.Multi ) - { - continue; - } - - var name = json[ nameof( IModGroup.Name ) ]?.ToObject< string >() ?? string.Empty; - if( name.Length == 0 ) - { - continue; - } - - - var options = json[ "Options" ]?.Children().ToList(); - if( options == null ) - { - continue; - } - - if( options.Count <= IModGroup.MaxMultiOptions ) - { - continue; - } - - Penumbra.Log.Information( $"Splitting multi group {name} in {mod.Name} due to {options.Count} being too many options." ); - var clone = json.DeepClone(); - reorder = true; - foreach( var o in options.Skip( IModGroup.MaxMultiOptions ) ) - { - o.Remove(); - } - - var newOptions = clone[ "Options" ]!.Children().ToList(); - foreach( var o in newOptions.Take( IModGroup.MaxMultiOptions ) ) - { - o.Remove(); - } - - var match = DuplicateNumber().Match( name ); - var startNumber = match.Success ? int.Parse( match.Groups[ 0 ].Value ) : 1; - name = match.Success ? name[ ..4 ] : name; - var oldName = $"{name}, Part {startNumber}"; - var oldPath = $"{baseDir.FullName}\\group_{idx:D3}_{oldName.RemoveInvalidPathSymbols().ToLowerInvariant()}.json"; - var newName = $"{name}, Part {startNumber + 1}"; - var newPath = $"{baseDir.FullName}\\group_{++idx:D3}_{newName.RemoveInvalidPathSymbols().ToLowerInvariant()}.json"; - json[ nameof( IModGroup.Name ) ] = oldName; - clone[ nameof( IModGroup.Name ) ] = newName; - - clone[ nameof( IModGroup.DefaultSettings ) ] = 0u; - - Penumbra.Log.Debug( $"Writing the first {IModGroup.MaxMultiOptions} options to {Path.GetFileName( oldPath )} after split." ); - using( var oldFile = File.CreateText( oldPath ) ) - { - using var j = new JsonTextWriter( oldFile ) - { - Formatting = Formatting.Indented, - }; - json.WriteTo( j ); - } - - Penumbra.Log.Debug( $"Writing the remaining {options.Count - IModGroup.MaxMultiOptions} options to {Path.GetFileName( newPath )} after split." ); - using( var newFile = File.CreateText( newPath ) ) - { - using var j = new JsonTextWriter( newFile ) - { - Formatting = Formatting.Indented, - }; - clone.WriteTo( j ); - } - - Penumbra.Log.Debug( - $"Deleting the old group file at {groupFile.Name} after splitting it into {Path.GetFileName( oldPath )} and {Path.GetFileName( newPath )}." ); - groupFile.Delete(); - } - catch( Exception ex ) - { - throw new Exception( $"Could not split multi group file {groupFile.Name} on .pmp import.", ex ); - } - } - } - - [GeneratedRegex( @", Part (\d+)$", RegexOptions.NonBacktracking )] - private static partial Regex DuplicateNumber(); + return new DirectoryInfo( newModFolder ); } + + /// + /// Create the name for a group or option subfolder based on its parent folder and given name. + /// subFolderName should never be empty, and the result is unique and contains no invalid symbols. + /// + public static DirectoryInfo? NewSubFolderName( DirectoryInfo parentFolder, string subFolderName ) + { + var newModFolderBase = NewOptionDirectory( parentFolder, subFolderName ); + var newModFolder = newModFolderBase.FullName.ObtainUniqueFile(); + return newModFolder.Length == 0 ? null : new DirectoryInfo( newModFolder ); + } + + /// Create a file for an option group from given data. + public static void CreateOptionGroup( DirectoryInfo baseFolder, GroupType type, string name, + int priority, int index, uint defaultSettings, string desc, IEnumerable< ISubMod > subMods ) + { + switch( type ) + { + case GroupType.Multi: + { + var group = new MultiModGroup() + { + Name = name, + Description = desc, + Priority = priority, + DefaultSettings = defaultSettings, + }; + group.PrioritizedOptions.AddRange( subMods.OfType< SubMod >().Select( ( s, idx ) => ( s, idx ) ) ); + Penumbra.SaveService.ImmediateSave(new ModSaveGroup(baseFolder, group, index)); + break; + } + case GroupType.Single: + { + var group = new SingleModGroup() + { + Name = name, + Description = desc, + Priority = priority, + DefaultSettings = defaultSettings, + }; + group.OptionData.AddRange( subMods.OfType< SubMod >() ); + Penumbra.SaveService.ImmediateSave(new ModSaveGroup(baseFolder, group, index)); + break; + } + } + } + + /// Create the data for a given sub mod from its data and the folder it is based on. + public static ISubMod CreateSubMod( DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option ) + { + var list = optionFolder.EnumerateFiles( "*.*", SearchOption.AllDirectories ) + .Select( f => ( Utf8GamePath.FromFile( f, optionFolder, out var gamePath, true ), gamePath, new FullPath( f ) ) ) + .Where( t => t.Item1 ); + + var mod = new SubMod( null! ) // Mod is irrelevant here, only used for saving. + { + Name = option.Name, + Description = option.Description, + }; + foreach( var (_, gamePath, file) in list ) + { + mod.FileData.TryAdd( gamePath, file ); + } + + mod.IncorporateMetaChanges( baseFolder, true ); + return mod; + } + + /// Create an empty sub mod for single groups with None options. + internal static ISubMod CreateEmptySubMod( string name ) + => new SubMod( null! ) // Mod is irrelevant here, only used for saving. + { + Name = name, + }; + + /// + /// Create the default data file from all unused files that were not handled before + /// and are used in sub mods. + /// + internal static void CreateDefaultFiles( DirectoryInfo directory ) + { + var mod = new Mod( directory ); + mod.Reload( Penumbra.ModManager, false, out _ ); + foreach( var file in mod.FindUnusedFiles() ) + { + if( Utf8GamePath.FromFile( new FileInfo( file.FullName ), directory, out var gamePath, true ) ) + mod._default.FileData.TryAdd( gamePath, file ); + } + + mod._default.IncorporateMetaChanges( directory, true ); + Penumbra.SaveService.ImmediateSave(new ModSaveGroup(mod, -1)); + } + + /// Return the name of a new valid directory based on the base directory and the given name. + public static DirectoryInfo NewOptionDirectory( DirectoryInfo baseDir, string optionName ) + => new(Path.Combine( baseDir.FullName, ReplaceBadXivSymbols( optionName ) )); + + /// Normalize for nicer names, and remove invalid symbols or invalid paths. + public static string ReplaceBadXivSymbols( string s, string replacement = "_" ) + { + switch( s ) + { + case ".": return replacement; + case "..": return replacement + replacement; + } + + StringBuilder sb = new(s.Length); + foreach( var c in s.Normalize( NormalizationForm.FormKC ) ) + { + if( c.IsInvalidInPath() ) + { + sb.Append( replacement ); + } + else + { + sb.Append( c ); + } + } + + return sb.ToString(); + } + + public static void SplitMultiGroups( DirectoryInfo baseDir ) + { + var mod = new Mod( baseDir ); + + var files = mod.GroupFiles.ToList(); + var idx = 0; + var reorder = false; + foreach( var groupFile in files ) + { + ++idx; + try + { + if( reorder ) + { + var newName = $"{baseDir.FullName}\\group_{idx:D3}{groupFile.Name[ 9.. ]}"; + Penumbra.Log.Debug( $"Moving {groupFile.Name} to {Path.GetFileName( newName )} due to reordering after multi group split." ); + groupFile.MoveTo( newName, false ); + } + } + catch( Exception ex ) + { + throw new Exception( "Could not reorder group file after splitting multi group on .pmp import.", ex ); + } + + try + { + var json = JObject.Parse( File.ReadAllText( groupFile.FullName ) ); + if( json[ nameof( IModGroup.Type ) ]?.ToObject< GroupType >() is not GroupType.Multi ) + { + continue; + } + + var name = json[ nameof( IModGroup.Name ) ]?.ToObject< string >() ?? string.Empty; + if( name.Length == 0 ) + { + continue; + } + + + var options = json[ "Options" ]?.Children().ToList(); + if( options == null ) + { + continue; + } + + if( options.Count <= IModGroup.MaxMultiOptions ) + { + continue; + } + + Penumbra.Log.Information( $"Splitting multi group {name} in {mod.Name} due to {options.Count} being too many options." ); + var clone = json.DeepClone(); + reorder = true; + foreach( var o in options.Skip( IModGroup.MaxMultiOptions ) ) + { + o.Remove(); + } + + var newOptions = clone[ "Options" ]!.Children().ToList(); + foreach( var o in newOptions.Take( IModGroup.MaxMultiOptions ) ) + { + o.Remove(); + } + + var match = DuplicateNumber().Match( name ); + var startNumber = match.Success ? int.Parse( match.Groups[ 0 ].Value ) : 1; + name = match.Success ? name[ ..4 ] : name; + var oldName = $"{name}, Part {startNumber}"; + var oldPath = $"{baseDir.FullName}\\group_{idx:D3}_{oldName.RemoveInvalidPathSymbols().ToLowerInvariant()}.json"; + var newName = $"{name}, Part {startNumber + 1}"; + var newPath = $"{baseDir.FullName}\\group_{++idx:D3}_{newName.RemoveInvalidPathSymbols().ToLowerInvariant()}.json"; + json[ nameof( IModGroup.Name ) ] = oldName; + clone[ nameof( IModGroup.Name ) ] = newName; + + clone[ nameof( IModGroup.DefaultSettings ) ] = 0u; + + Penumbra.Log.Debug( $"Writing the first {IModGroup.MaxMultiOptions} options to {Path.GetFileName( oldPath )} after split." ); + using( var oldFile = File.CreateText( oldPath ) ) + { + using var j = new JsonTextWriter( oldFile ) + { + Formatting = Formatting.Indented, + }; + json.WriteTo( j ); + } + + Penumbra.Log.Debug( $"Writing the remaining {options.Count - IModGroup.MaxMultiOptions} options to {Path.GetFileName( newPath )} after split." ); + using( var newFile = File.CreateText( newPath ) ) + { + using var j = new JsonTextWriter( newFile ) + { + Formatting = Formatting.Indented, + }; + clone.WriteTo( j ); + } + + Penumbra.Log.Debug( + $"Deleting the old group file at {groupFile.Name} after splitting it into {Path.GetFileName( oldPath )} and {Path.GetFileName( newPath )}." ); + groupFile.Delete(); + } + catch( Exception ex ) + { + throw new Exception( $"Could not split multi group file {groupFile.Name} on .pmp import.", ex ); + } + } + } + + [GeneratedRegex(@", Part (\d+)$", RegexOptions.NonBacktracking )] + private static partial Regex DuplicateNumber(); } \ No newline at end of file diff --git a/Penumbra/Mods/Mod.Files.cs b/Penumbra/Mods/Mod.Files.cs index 078e5a95..cd9071bd 100644 --- a/Penumbra/Mods/Mod.Files.cs +++ b/Penumbra/Mods/Mod.Files.cs @@ -114,13 +114,13 @@ public partial class Mod _default.WriteTexToolsMeta(ModPath); foreach (var group in Groups) { - var dir = Creator.NewOptionDirectory(ModPath, group.Name); + var dir = ModCreator.NewOptionDirectory(ModPath, group.Name); if (!dir.Exists) dir.Create(); foreach (var option in group.OfType()) { - var optionDir = Creator.NewOptionDirectory(dir, option.Name); + var optionDir = ModCreator.NewOptionDirectory(dir, option.Name); if (!optionDir.Exists) optionDir.Create(); diff --git a/Penumbra/Mods/Mod.LocalData.cs b/Penumbra/Mods/Mod.LocalData.cs deleted file mode 100644 index af79191f..00000000 --- a/Penumbra/Mods/Mod.LocalData.cs +++ /dev/null @@ -1,77 +0,0 @@ -using Newtonsoft.Json.Linq; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Newtonsoft.Json; -using Penumbra.Services; -using Penumbra.Util; - -namespace Penumbra.Mods; - -public sealed partial class Mod -{ - public long ImportDate { get; internal set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds(); - - public IReadOnlyList LocalTags { get; private set; } = Array.Empty(); - - public string Note { get; internal set; } = string.Empty; - public bool Favorite { get; internal set; } = false; - - internal ModDataChangeType UpdateTags(IEnumerable? newModTags, IEnumerable? newLocalTags) - { - if (newModTags == null && newLocalTags == null) - return 0; - - ModDataChangeType type = 0; - if (newModTags != null) - { - var modTags = newModTags.Where(t => t.Length > 0).Distinct().ToArray(); - if (!modTags.SequenceEqual(ModTags)) - { - newLocalTags ??= LocalTags; - ModTags = modTags; - type |= ModDataChangeType.ModTags; - } - } - - if (newLocalTags != null) - { - var localTags = newLocalTags!.Where(t => t.Length > 0 && !ModTags.Contains(t)).Distinct().ToArray(); - if (!localTags.SequenceEqual(LocalTags)) - { - LocalTags = localTags; - type |= ModDataChangeType.LocalTags; - } - } - - return type; - } - - internal readonly struct ModData : ISavable - { - public const int FileVersion = 3; - - private readonly Mod _mod; - - public ModData(Mod mod) - => _mod = mod; - - public string ToFilename(FilenameService fileNames) - => fileNames.LocalDataFile(_mod); - - public void Save(StreamWriter writer) - { - var jObject = new JObject - { - { nameof(FileVersion), JToken.FromObject(FileVersion) }, - { nameof(ImportDate), JToken.FromObject(_mod.ImportDate) }, - { nameof(LocalTags), JToken.FromObject(_mod.LocalTags) }, - { nameof(Note), JToken.FromObject(_mod.Note) }, - { nameof(Favorite), JToken.FromObject(_mod.Favorite) }, - }; - using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; - jObject.WriteTo(jWriter); - } - } -} diff --git a/Penumbra/Mods/Mod.Meta.Migration.cs b/Penumbra/Mods/Mod.Meta.Migration.cs deleted file mode 100644 index d993cef0..00000000 --- a/Penumbra/Mods/Mod.Meta.Migration.cs +++ /dev/null @@ -1,246 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using OtterGui; -using Penumbra.Api.Enums; -using Penumbra.String.Classes; - -namespace Penumbra.Mods; - -public sealed partial class Mod -{ - public static partial class Migration - { - [GeneratedRegex(@"group_\d{3}_", RegexOptions.Compiled | RegexOptions.NonBacktracking | RegexOptions.ExplicitCapture)] - private static partial Regex GroupRegex(); - - [GeneratedRegex("^group_", RegexOptions.Compiled)] - private static partial Regex GroupStartRegex(); - - public static bool Migrate(Mod mod, JObject json, ref uint fileVersion) - => MigrateV0ToV1(mod, json, ref fileVersion) || MigrateV1ToV2(mod, ref fileVersion) || MigrateV2ToV3(mod, ref fileVersion); - - private static bool MigrateV2ToV3(Mod _, ref uint fileVersion) - { - if (fileVersion > 2) - return false; - - // Remove import time. - fileVersion = 3; - return true; - } - - private static bool MigrateV1ToV2(Mod mod, ref uint fileVersion) - { - if (fileVersion > 1) - return false; - - if (!mod.GroupFiles.All(g => GroupRegex().IsMatch(g.Name))) - foreach (var (group, index) in mod.GroupFiles.WithIndex().ToArray()) - { - var newName = GroupStartRegex().Replace(group.Name, $"group_{index + 1:D3}_"); - try - { - if (newName != group.Name) - group.MoveTo(Path.Combine(group.DirectoryName ?? string.Empty, newName), false); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not rename group file {group.Name} to {newName} during migration:\n{e}"); - } - } - - fileVersion = 2; - - return true; - } - - private static bool MigrateV0ToV1(Mod mod, JObject json, ref uint fileVersion) - { - if (fileVersion > 0) - return false; - - var swaps = json["FileSwaps"]?.ToObject>() - ?? new Dictionary(); - var groups = json["Groups"]?.ToObject>() ?? new Dictionary(); - var priority = 1; - var seenMetaFiles = new HashSet(); - foreach (var group in groups.Values) - ConvertGroup(mod, group, ref priority, seenMetaFiles); - - foreach (var unusedFile in mod.FindUnusedFiles().Where(f => !seenMetaFiles.Contains(f))) - { - if (unusedFile.ToGamePath(mod.ModPath, out var gamePath) - && !mod._default.FileData.TryAdd(gamePath, unusedFile)) - Penumbra.Log.Error($"Could not add {gamePath} because it already points to {mod._default.FileData[gamePath]}."); - } - - mod._default.FileSwapData.Clear(); - mod._default.FileSwapData.EnsureCapacity(swaps.Count); - foreach (var (gamePath, swapPath) in swaps) - mod._default.FileSwapData.Add(gamePath, swapPath); - - mod._default.IncorporateMetaChanges(mod.ModPath, true); - foreach (var (_, index) in mod.Groups.WithIndex()) - Penumbra.SaveService.ImmediateSave(new ModSaveGroup(mod, index)); - - // Delete meta files. - foreach (var file in seenMetaFiles.Where(f => f.Exists)) - { - try - { - File.Delete(file.FullName); - } - catch (Exception e) - { - Penumbra.Log.Warning($"Could not delete meta file {file.FullName} during migration:\n{e}"); - } - } - - // Delete old meta files. - var oldMetaFile = Path.Combine(mod.ModPath.FullName, "metadata_manipulations.json"); - if (File.Exists(oldMetaFile)) - try - { - File.Delete(oldMetaFile); - } - catch (Exception e) - { - Penumbra.Log.Warning($"Could not delete old meta file {oldMetaFile} during migration:\n{e}"); - } - - fileVersion = 1; - Penumbra.SaveService.ImmediateSave(new ModSaveGroup(mod, -1)); - - return true; - } - - private static void ConvertGroup(Mod mod, OptionGroupV0 group, ref int priority, HashSet seenMetaFiles) - { - if (group.Options.Count == 0) - return; - - switch (group.SelectionType) - { - case GroupType.Multi: - - var optionPriority = 0; - var newMultiGroup = new MultiModGroup() - { - Name = group.GroupName, - Priority = priority++, - Description = string.Empty, - }; - mod._groups.Add(newMultiGroup); - foreach (var option in group.Options) - newMultiGroup.PrioritizedOptions.Add((SubModFromOption(mod, option, seenMetaFiles), optionPriority++)); - - break; - case GroupType.Single: - if (group.Options.Count == 1) - { - AddFilesToSubMod(mod._default, mod.ModPath, group.Options[0], seenMetaFiles); - return; - } - - var newSingleGroup = new SingleModGroup() - { - Name = group.GroupName, - Priority = priority++, - Description = string.Empty, - }; - mod._groups.Add(newSingleGroup); - foreach (var option in group.Options) - newSingleGroup.OptionData.Add(SubModFromOption(mod, option, seenMetaFiles)); - - break; - } - } - - private static void AddFilesToSubMod(SubMod mod, DirectoryInfo basePath, OptionV0 option, HashSet seenMetaFiles) - { - foreach (var (relPath, gamePaths) in option.OptionFiles) - { - var fullPath = new FullPath(basePath, relPath); - foreach (var gamePath in gamePaths) - mod.FileData.TryAdd(gamePath, fullPath); - - if (fullPath.Extension is ".meta" or ".rgsp") - seenMetaFiles.Add(fullPath); - } - } - - private static SubMod SubModFromOption(Mod mod, OptionV0 option, HashSet seenMetaFiles) - { - var subMod = new SubMod(mod) { Name = option.OptionName }; - AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles); - subMod.IncorporateMetaChanges(mod.ModPath, false); - return subMod; - } - - private struct OptionV0 - { - public string OptionName = string.Empty; - public string OptionDesc = string.Empty; - - [JsonProperty(ItemConverterType = typeof(SingleOrArrayConverter))] - public Dictionary> OptionFiles = new(); - - public OptionV0() - { } - } - - private struct OptionGroupV0 - { - public string GroupName = string.Empty; - - [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] - public GroupType SelectionType = GroupType.Single; - - public List Options = new(); - - public OptionGroupV0() - { } - } - - // Not used anymore, but required for migration. - private class SingleOrArrayConverter : JsonConverter - { - public override bool CanConvert(Type objectType) - => objectType == typeof(HashSet); - - public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - { - var token = JToken.Load(reader); - - if (token.Type == JTokenType.Array) - return token.ToObject>() ?? new HashSet(); - - var tmp = token.ToObject(); - return tmp != null - ? new HashSet { tmp } - : new HashSet(); - } - - public override bool CanWrite - => true; - - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - { - writer.WriteStartArray(); - if (value != null) - { - var v = (HashSet)value; - foreach (var val in v) - serializer.Serialize(writer, val?.ToString()); - } - - writer.WriteEndArray(); - } - } - } -} diff --git a/Penumbra/Mods/Mod.Meta.cs b/Penumbra/Mods/Mod.Meta.cs deleted file mode 100644 index 3b716298..00000000 --- a/Penumbra/Mods/Mod.Meta.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using OtterGui.Classes; -using Penumbra.Services; -using Penumbra.Util; - -namespace Penumbra.Mods; - -public sealed partial class Mod : IMod -{ - public static readonly TemporaryMod ForcedFiles = new() - { - Name = "Forced Files", - Index = -1, - Priority = int.MaxValue, - }; - - public LowerString Name { get; internal set; } = "New Mod"; - public LowerString Author { get; internal set; } = LowerString.Empty; - public string Description { get; internal set; } = string.Empty; - public string Version { get; internal set; } = string.Empty; - public string Website { get; internal set; } = string.Empty; - public IReadOnlyList ModTags { get; internal set; } = Array.Empty(); - - public override string ToString() - => Name.Text; - - internal readonly struct ModMeta : ISavable - { - public const uint FileVersion = 3; - - private readonly Mod _mod; - - public ModMeta(Mod mod) - => _mod = mod; - - public string ToFilename(FilenameService fileNames) - => fileNames.ModMetaPath(_mod); - - public void Save(StreamWriter writer) - { - var jObject = new JObject - { - { nameof(FileVersion), JToken.FromObject(FileVersion) }, - { nameof(Name), JToken.FromObject(_mod.Name) }, - { nameof(Author), JToken.FromObject(_mod.Author) }, - { nameof(Description), JToken.FromObject(_mod.Description) }, - { nameof(Version), JToken.FromObject(_mod.Version) }, - { nameof(Website), JToken.FromObject(_mod.Website) }, - { nameof(ModTags), JToken.FromObject(_mod.ModTags) }, - }; - using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; - jObject.WriteTo(jWriter); - } - } -} diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs new file mode 100644 index 00000000..bc7a2c4d --- /dev/null +++ b/Penumbra/Mods/Mod.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using OtterGui.Classes; + +namespace Penumbra.Mods; + +public sealed partial class Mod : IMod +{ + public static readonly TemporaryMod ForcedFiles = new() + { + Name = "Forced Files", + Index = -1, + Priority = int.MaxValue, + }; + + // Meta Data + public LowerString Name { get; internal set; } = "New Mod"; + public LowerString Author { get; internal set; } = LowerString.Empty; + public string Description { get; internal set; } = string.Empty; + public string Version { get; internal set; } = string.Empty; + public string Website { get; internal set; } = string.Empty; + public IReadOnlyList ModTags { get; internal set; } = Array.Empty(); + + + // Local Data + public long ImportDate { get; internal set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds(); + public IReadOnlyList LocalTags { get; internal set; } = Array.Empty(); + public string Note { get; internal set; } = string.Empty; + public bool Favorite { get; internal set; } = false; + + + // Access + public override string ToString() + => Name.Text; +} diff --git a/Penumbra/Mods/ModCache.cs b/Penumbra/Mods/ModCache.cs index 7602fb95..e6848034 100644 --- a/Penumbra/Mods/ModCache.cs +++ b/Penumbra/Mods/ModCache.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace Penumbra.Mods.Manager; +namespace Penumbra.Mods; public class ModCache { diff --git a/Penumbra/Mods/ModLocalData.cs b/Penumbra/Mods/ModLocalData.cs new file mode 100644 index 00000000..71aae013 --- /dev/null +++ b/Penumbra/Mods/ModLocalData.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.Mods.Manager; +using Penumbra.Services; +using Penumbra.Util; + +namespace Penumbra.Mods; + +public readonly struct ModLocalData : ISavable +{ + public const int FileVersion = 3; + + private readonly Mod _mod; + + public ModLocalData(Mod mod) + => _mod = mod; + + public string ToFilename(FilenameService fileNames) + => fileNames.LocalDataFile(_mod); + + public void Save(StreamWriter writer) + { + var jObject = new JObject + { + { nameof(FileVersion), JToken.FromObject(FileVersion) }, + { nameof(Mod.ImportDate), JToken.FromObject(_mod.ImportDate) }, + { nameof(Mod.LocalTags), JToken.FromObject(_mod.LocalTags) }, + { nameof(Mod.Note), JToken.FromObject(_mod.Note) }, + { nameof(Mod.Favorite), JToken.FromObject(_mod.Favorite) }, + }; + using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + jObject.WriteTo(jWriter); + } + + internal static ModDataChangeType UpdateTags(Mod mod, IEnumerable? newModTags, IEnumerable? newLocalTags) + { + if (newModTags == null && newLocalTags == null) + return 0; + + ModDataChangeType type = 0; + if (newModTags != null) + { + var modTags = newModTags.Where(t => t.Length > 0).Distinct().ToArray(); + if (!modTags.SequenceEqual(mod.ModTags)) + { + newLocalTags ??= mod.LocalTags; + mod.ModTags = modTags; + type |= ModDataChangeType.ModTags; + } + } + + if (newLocalTags != null) + { + var localTags = newLocalTags!.Where(t => t.Length > 0 && !mod.ModTags.Contains(t)).Distinct().ToArray(); + if (!localTags.SequenceEqual(mod.LocalTags)) + { + mod.LocalTags = localTags; + type |= ModDataChangeType.LocalTags; + } + } + + return type; + } +} diff --git a/Penumbra/Mods/ModMeta.cs b/Penumbra/Mods/ModMeta.cs new file mode 100644 index 00000000..66979345 --- /dev/null +++ b/Penumbra/Mods/ModMeta.cs @@ -0,0 +1,36 @@ +using System.IO; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.Services; +using Penumbra.Util; + +namespace Penumbra.Mods; + +public readonly struct ModMeta : ISavable +{ + public const uint FileVersion = 3; + + private readonly Mod _mod; + + public ModMeta(Mod mod) + => _mod = mod; + + public string ToFilename(FilenameService fileNames) + => fileNames.ModMetaPath(_mod); + + public void Save(StreamWriter writer) + { + var jObject = new JObject + { + { nameof(FileVersion), JToken.FromObject(FileVersion) }, + { nameof(Mod.Name), JToken.FromObject(_mod.Name) }, + { nameof(Mod.Author), JToken.FromObject(_mod.Author) }, + { nameof(Mod.Description), JToken.FromObject(_mod.Description) }, + { nameof(Mod.Version), JToken.FromObject(_mod.Version) }, + { nameof(Mod.Website), JToken.FromObject(_mod.Website) }, + { nameof(Mod.ModTags), JToken.FromObject(_mod.ModTags) }, + }; + using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + jObject.WriteTo(jWriter); + } +} diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index d7265093..3f0664cb 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -50,7 +50,7 @@ public class TemporaryMod : IMod DirectoryInfo? dir = null; try { - dir = Mod.Creator.CreateModFolder( Penumbra.ModManager.BasePath, collection.Name ); + dir = ModCreator.CreateModFolder( Penumbra.ModManager.BasePath, collection.Name ); var fileDir = Directory.CreateDirectory( Path.Combine( dir.FullName, "files" ) ); modManager.DataEditor.CreateMeta( dir, collection.Name, character ?? Penumbra.Config.DefaultModAuthor, $"Mod generated from temporary collection {collection.Name} for {character ?? "Unknown Character"}.", null, null ); diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index c13ede76..24fcd271 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -24,7 +24,6 @@ using Penumbra.GameData.Actors; using Penumbra.GameData.Data; using Penumbra.Interop.ResourceLoading; using Penumbra.Interop.PathResolving; -using Penumbra.Mods; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using DalamudUtil = Dalamud.Utility.Util; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 35737920..17c58dc6 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -269,9 +269,9 @@ public class ItemSwapTab : IDisposable, ITab private void CreateMod() { - var newDir = Mod.Creator.CreateModFolder(_modManager.BasePath, _newModName); + var newDir = ModCreator.CreateModFolder(_modManager.BasePath, _newModName); _modManager.DataEditor.CreateMeta(newDir, _newModName, _config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty); - Mod.Creator.CreateDefaultFiles(newDir); + ModCreator.CreateDefaultFiles(newDir); _modManager.AddMod(newDir); if (!_swapData.WriteMod(_modManager, _modManager[^1], _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps)) @@ -290,7 +290,7 @@ public class ItemSwapTab : IDisposable, ITab try { optionFolderName = - Mod.Creator.NewSubFolderName(new DirectoryInfo(Path.Combine(_mod.ModPath.FullName, _selectedGroup?.Name ?? _newGroupName)), + ModCreator.NewSubFolderName(new DirectoryInfo(Path.Combine(_mod.ModPath.FullName, _selectedGroup?.Name ?? _newGroupName)), _newOptionName); if (optionFolderName?.Exists == true) throw new Exception($"The folder {optionFolderName.FullName} for the option already exists."); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 2d4415d9..b90d607e 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -217,13 +217,13 @@ public partial class ModEditWindow var fullName = subMod.FullName; if (fullName.EndsWith(": " + name)) { - path = Mod.Creator.NewOptionDirectory(path, fullName[..^(name.Length + 2)]); - path = Mod.Creator.NewOptionDirectory(path, name); + path = ModCreator.NewOptionDirectory(path, fullName[..^(name.Length + 2)]); + path = ModCreator.NewOptionDirectory(path, name); subDirs = 2; } else { - path = Mod.Creator.NewOptionDirectory(path, fullName); + path = ModCreator.NewOptionDirectory(path, fullName); subDirs = 1; } diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index bc84f757..72e9a362 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -131,9 +131,9 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector Queue a save for the next framework tick. public void QueueSave(ISavable value) { - var file = value.ToFilename(_fileNames); + var file = value.ToFilename(FileNames); _framework.RegisterDelayed(value.GetType().Name + file, () => { ImmediateSave(value); @@ -53,7 +54,7 @@ public class SaveService /// Immediately trigger a save. public void ImmediateSave(ISavable value) { - var name = value.ToFilename(_fileNames); + var name = value.ToFilename(FileNames); try { if (name.Length == 0) @@ -76,7 +77,7 @@ public class SaveService public void ImmediateDelete(ISavable value) { - var name = value.ToFilename(_fileNames); + var name = value.ToFilename(FileNames); try { if (name.Length == 0) @@ -99,7 +100,7 @@ public class SaveService /// Immediately delete all existing option group files for a mod and save them anew. public void SaveAllOptionGroups(Mod mod) { - foreach (var file in _fileNames.GetOptionGroupFiles(mod)) + foreach (var file in FileNames.GetOptionGroupFiles(mod)) { try {