using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui.Filesystem; using OtterGui.Services; using Penumbra.Api.Enums; using Penumbra.Collections; using Penumbra.Collections.Manager; using Penumbra.Enums; using Penumbra.Interop.Services; using Penumbra.Mods; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; using Penumbra.UI; using Penumbra.UI.Classes; using Penumbra.UI.ResourceWatcher; using Penumbra.UI.Tabs; namespace Penumbra.Services; /// /// Contains everything to migrate from older versions of the config to the current, /// including deprecated fields. /// public class ConfigMigrationService(SaveService saveService) : IService { private Configuration _config = null!; private JObject _data = null!; public string CurrentCollection = ModCollection.DefaultCollectionName; public string DefaultCollection = ModCollection.DefaultCollectionName; public string ForcedCollection = string.Empty; public Dictionary CharacterCollections = []; public Dictionary ModSortOrder = []; public bool InvertModListOrder; public bool SortFoldersFirst; public SortModeV3 SortMode = SortModeV3.FoldersFirst; /// Add missing colors to the dictionary if necessary. private static void AddColors(Configuration config, bool forceSave) { var save = false; foreach (var color in Enum.GetValues()) save |= config.Colors.TryAdd(color, color.Data().DefaultColor); if (save || forceSave) config.Save(); Colors.SetColors(config); } public void Migrate(CharacterUtility utility, Configuration config) { _config = config; // Do this on every migration from now on for a while // because it stayed alive for a bunch of people for some reason. DeleteMetaTmp(); if (config.Version >= Configuration.Constants.CurrentVersion || !File.Exists(saveService.FileNames.ConfigFile)) { AddColors(config, false); return; } _data = JObject.Parse(File.ReadAllText(saveService.FileNames.ConfigFile)); CreateBackup(); Version0To1(); Version1To2(utility); Version2To3(); Version3To4(); Version4To5(); Version5To6(); Version6To7(); Version7To8(); AddColors(config, true); } // Migrate to ephemeral config. private void Version7To8() { if (_config.Version != 7) return; _config.Version = 8; _config.Ephemeral.Version = 8; _config.Ephemeral.LastSeenVersion = _data["LastSeenVersion"]?.ToObject() ?? _config.Ephemeral.LastSeenVersion; _config.Ephemeral.DebugSeparateWindow = _data["DebugSeparateWindow"]?.ToObject() ?? _config.Ephemeral.DebugSeparateWindow; _config.Ephemeral.TutorialStep = _data["TutorialStep"]?.ToObject() ?? _config.Ephemeral.TutorialStep; _config.Ephemeral.EnableResourceLogging = _data["EnableResourceLogging"]?.ToObject() ?? _config.Ephemeral.EnableResourceLogging; _config.Ephemeral.ResourceLoggingFilter = _data["ResourceLoggingFilter"]?.ToObject() ?? _config.Ephemeral.ResourceLoggingFilter; _config.Ephemeral.EnableResourceWatcher = _data["EnableResourceWatcher"]?.ToObject() ?? _config.Ephemeral.EnableResourceWatcher; _config.Ephemeral.OnlyAddMatchingResources = _data["OnlyAddMatchingResources"]?.ToObject() ?? _config.Ephemeral.OnlyAddMatchingResources; _config.Ephemeral.ResourceWatcherResourceTypes = _data["ResourceWatcherResourceTypes"]?.ToObject() ?? _config.Ephemeral.ResourceWatcherResourceTypes; _config.Ephemeral.ResourceWatcherResourceCategories = _data["ResourceWatcherResourceCategories"]?.ToObject() ?? _config.Ephemeral.ResourceWatcherResourceCategories; _config.Ephemeral.ResourceWatcherRecordTypes = _data["ResourceWatcherRecordTypes"]?.ToObject() ?? _config.Ephemeral.ResourceWatcherRecordTypes; _config.Ephemeral.CollectionPanel = _data["CollectionPanel"]?.ToObject() ?? _config.Ephemeral.CollectionPanel; _config.Ephemeral.SelectedTab = _data["SelectedTab"]?.ToObject() ?? _config.Ephemeral.SelectedTab; _config.Ephemeral.ChangedItemFilter = _data["ChangedItemFilter"]?.ToObject() ?? _config.Ephemeral.ChangedItemFilter; _config.Ephemeral.FixMainWindow = _data["FixMainWindow"]?.ToObject() ?? _config.Ephemeral.FixMainWindow; _config.Ephemeral.Save(); } // Gendered special collections were added. private void Version6To7() { if (_config.Version != 6) return; ActiveCollectionMigration.MigrateUngenderedCollections(saveService.FileNames); _config.Version = 7; } // A new tutorial step was inserted in the middle. // The UI collection and a new tutorial for it was added. // The migration for the UI collection itself happens in the ActiveCollections file. private void Version5To6() { if (_config.Version != 5) return; if (_config.Ephemeral.TutorialStep == 25) _config.Ephemeral.TutorialStep = 27; _config.Version = 6; } // Mod backup extension was changed from .zip to .pmp. // Actual migration takes place in ModManager. private void Version4To5() { if (_config.Version != 4) return; ModBackup.MigrateModBackups = true; _config.Version = 5; } // SortMode was changed from an enum to a type. private void Version3To4() { if (_config.Version != 3) return; SortMode = _data[nameof(SortMode)]?.ToObject() ?? SortMode; _config.SortMode = SortMode switch { SortModeV3.FoldersFirst => ISortMode.FoldersFirst, SortModeV3.Lexicographical => ISortMode.Lexicographical, SortModeV3.InverseFoldersFirst => ISortMode.InverseFoldersFirst, SortModeV3.InverseLexicographical => ISortMode.InverseLexicographical, SortModeV3.FoldersLast => ISortMode.FoldersLast, SortModeV3.InverseFoldersLast => ISortMode.InverseFoldersLast, SortModeV3.InternalOrder => ISortMode.InternalOrder, SortModeV3.InternalOrderInverse => ISortMode.InverseInternalOrder, _ => ISortMode.FoldersFirst, }; _config.Version = 4; } // SortFoldersFirst was changed from a bool to the enum SortMode. private void Version2To3() { if (_config.Version != 2) return; SortFoldersFirst = _data[nameof(SortFoldersFirst)]?.ToObject() ?? false; SortMode = SortFoldersFirst ? SortModeV3.FoldersFirst : SortModeV3.Lexicographical; _config.Version = 3; } // The forced collection was removed due to general inheritance. // Sort Order was moved to a separate file and may contain empty folders. // Active collections in general were moved to their own file. // Delete the penumbrametatmp folder if it exists. private void Version1To2(CharacterUtility utility) { if (_config.Version != 1) return; // Ensure the right meta files are loaded. DeleteMetaTmp(); if (utility.Ready) utility.LoadCharacterResources(); ResettleSortOrder(); ResettleCollectionSettings(); ResettleForcedCollection(); _config.Version = 2; } private void DeleteMetaTmp() { var path = Path.Combine(_config.ModDirectory, "penumbrametatmp"); if (!Directory.Exists(path)) return; try { Directory.Delete(path, true); } catch (Exception e) { Penumbra.Log.Error($"Could not delete the outdated penumbrametatmp folder:\n{e}"); } } private void ResettleForcedCollection() { ForcedCollection = _data[nameof(ForcedCollection)]?.ToObject() ?? ForcedCollection; if (ForcedCollection.Length <= 0) return; // Add the previous forced collection to all current collections except itself as an inheritance. foreach (var collection in saveService.FileNames.CollectionFiles) { try { var jObject = JObject.Parse(File.ReadAllText(collection.FullName)); if (jObject[nameof(ModCollection.Name)]?.ToObject() == ForcedCollection) continue; jObject[nameof(ModCollection.DirectlyInheritsFrom)] = JToken.FromObject(new List { ForcedCollection }); File.WriteAllText(collection.FullName, jObject.ToString()); } catch (Exception e) { Penumbra.Log.Error( $"Could not transfer forced collection {ForcedCollection} to inheritance of collection {collection}:\n{e}"); } } } // Move the current sort order to its own file. private void ResettleSortOrder() { ModSortOrder = _data[nameof(ModSortOrder)]?.ToObject>() ?? ModSortOrder; var file = saveService.FileNames.FilesystemFile; using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew); using var writer = new StreamWriter(stream); using var j = new JsonTextWriter(writer); j.Formatting = Formatting.Indented; j.WriteStartObject(); j.WritePropertyName("Data"); j.WriteStartObject(); foreach (var (mod, path) in ModSortOrder.Where(kvp => Directory.Exists(Path.Combine(_config.ModDirectory, kvp.Key)))) { j.WritePropertyName(mod, true); j.WriteValue(path); } j.WriteEndObject(); j.WritePropertyName("EmptyFolders"); j.WriteStartArray(); j.WriteEndArray(); j.WriteEndObject(); } // Move the active collections to their own file. private void ResettleCollectionSettings() { CurrentCollection = _data[nameof(CurrentCollection)]?.ToObject() ?? CurrentCollection; DefaultCollection = _data[nameof(DefaultCollection)]?.ToObject() ?? DefaultCollection; CharacterCollections = _data[nameof(CharacterCollections)]?.ToObject>() ?? CharacterCollections; SaveActiveCollectionsV0(DefaultCollection, CurrentCollection, DefaultCollection, CharacterCollections.Select(kvp => (kvp.Key, kvp.Value)), Array.Empty<(CollectionType, string)>()); } // Outdated saving using the Characters list. private void SaveActiveCollectionsV0(string def, string ui, string current, IEnumerable<(string, string)> characters, IEnumerable<(CollectionType, string)> special) { var file = saveService.FileNames.ActiveCollectionsFile; try { using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew); using var writer = new StreamWriter(stream); using var j = new JsonTextWriter(writer); j.Formatting = Formatting.Indented; j.WriteStartObject(); j.WritePropertyName(nameof(ActiveCollectionData.Default)); j.WriteValue(def); j.WritePropertyName(nameof(ActiveCollectionData.Interface)); j.WriteValue(ui); j.WritePropertyName(nameof(ActiveCollectionData.Current)); j.WriteValue(current); foreach (var (type, collection) in special) { j.WritePropertyName(type.ToString()); j.WriteValue(collection); } j.WritePropertyName("Characters"); j.WriteStartObject(); foreach (var (character, collection) in characters) { j.WritePropertyName(character, true); j.WriteValue(collection); } j.WriteEndObject(); j.WriteEndObject(); Penumbra.Log.Verbose("Active Collections saved."); } catch (Exception e) { Penumbra.Log.Error($"Could not save active collections to file {file}:\n{e}"); } } // Collections were introduced and the previous CurrentCollection got put into ModDirectory. private void Version0To1() { if (_config.Version != 0) return; _config.ModDirectory = _data[nameof(CurrentCollection)]?.ToObject() ?? string.Empty; _config.Version = 1; ResettleCollectionJson(); } /// Move the previous mod configurations to a new default collection file. private void ResettleCollectionJson() { var collectionJson = new FileInfo(Path.Combine(_config.ModDirectory, "collection.json")); if (!collectionJson.Exists) return; var defaultCollectionFile = new FileInfo(saveService.FileNames.CollectionFile(ModCollection.DefaultCollectionName)); if (defaultCollectionFile.Exists) return; try { var text = File.ReadAllText(collectionJson.FullName); var data = JArray.Parse(text); var maxPriority = 0; var dict = new Dictionary(); foreach (var setting in data.Cast()) { var modName = (string)setting["FolderName"]!; var enabled = (bool)setting["Enabled"]!; var priority = (int)setting["Priority"]!; var settings = setting["Settings"]!.ToObject>() ?? setting["Conf"]!.ToObject>(); dict[modName] = new ModSettings.SavedSettings() { Enabled = enabled, Priority = priority, Settings = settings!, }; maxPriority = Math.Max(maxPriority, priority); } InvertModListOrder = _data[nameof(InvertModListOrder)]?.ToObject() ?? InvertModListOrder; if (!InvertModListOrder) dict = dict.ToDictionary(kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority }); var emptyStorage = new ModStorage(); var collection = ModCollection.CreateFromData(saveService, emptyStorage, ModCollection.DefaultCollectionName, 0, 1, dict, Array.Empty()); saveService.ImmediateSaveSync(new ModCollectionSave(emptyStorage, collection)); } catch (Exception e) { Penumbra.Log.Error($"Could not migrate the old collection file to new collection files:\n{e}"); throw; } } // Create a backup of the configuration file specifically. private void CreateBackup() { var name = saveService.FileNames.ConfigFile; var bakName = name + ".bak"; try { File.Copy(name, bakName, true); } catch (Exception e) { Penumbra.Log.Error($"Could not create backup copy of config at {bakName}:\n{e}"); } } public enum SortModeV3 : byte { FoldersFirst = 0x00, Lexicographical = 0x01, InverseFoldersFirst = 0x02, InverseLexicographical = 0x03, FoldersLast = 0x04, InverseFoldersLast = 0x05, InternalOrder = 0x06, InternalOrderInverse = 0x07, } }