diff --git a/Penumbra/Api/IpcTester.cs b/Penumbra/Api/IpcTester.cs index 104a2079..c98ac854 100644 --- a/Penumbra/Api/IpcTester.cs +++ b/Penumbra/Api/IpcTester.cs @@ -38,7 +38,7 @@ public class IpcTester : IDisposable private readonly ModSettings _modSettings; private readonly Temporary _temporary; - public IpcTester(DalamudPluginInterface pi, PenumbraIpcProviders ipcProviders, Mod.Manager modManager) + public IpcTester(DalamudPluginInterface pi, PenumbraIpcProviders ipcProviders, ModManager modManager) { _ipcProviders = ipcProviders; _pluginState = new PluginState(pi); @@ -1139,9 +1139,9 @@ public class IpcTester : IDisposable private class Temporary { private readonly DalamudPluginInterface _pi; - private readonly Mod.Manager _modManager; + private readonly ModManager _modManager; - public Temporary(DalamudPluginInterface pi, Mod.Manager modManager) + public Temporary(DalamudPluginInterface pi, ModManager modManager) { _pi = pi; _modManager = modManager; diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 913deaf3..5b5df883 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -93,10 +93,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi private Penumbra _penumbra; private Lumina.GameData? _lumina; - private Mod.Manager _modManager; + private ModManager _modManager; private ResourceLoader _resourceLoader; private Configuration _config; - private ModCollection.Manager _collectionManager; + private CollectionManager _collectionManager; private DalamudServices _dalamud; private TempCollectionManager _tempCollections; private TempModManager _tempMods; @@ -104,8 +104,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi private CollectionResolver _collectionResolver; private CutsceneService _cutsceneService; - public unsafe PenumbraApi(CommunicatorService communicator, Penumbra penumbra, Mod.Manager modManager, ResourceLoader resourceLoader, - Configuration config, ModCollection.Manager collectionManager, DalamudServices dalamud, TempCollectionManager tempCollections, + public unsafe PenumbraApi(CommunicatorService communicator, Penumbra penumbra, ModManager modManager, ResourceLoader resourceLoader, + Configuration config, CollectionManager collectionManager, DalamudServices dalamud, TempCollectionManager tempCollections, TempModManager tempMods, ActorService actors, CollectionResolver collectionResolver, CutsceneService cutsceneService) { _communicator = communicator; @@ -1021,7 +1021,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi // Resolve a path given by string for a specific collection. [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private string ResolvePath(string path, Mod.Manager _, ModCollection collection) + private string ResolvePath(string path, ModManager _, ModCollection collection) { if (!_config.EnableMods) return path; diff --git a/Penumbra/Api/PenumbraIpcProviders.cs b/Penumbra/Api/PenumbraIpcProviders.cs index 50768ddf..e6d135bb 100644 --- a/Penumbra/Api/PenumbraIpcProviders.cs +++ b/Penumbra/Api/PenumbraIpcProviders.cs @@ -112,7 +112,7 @@ public class PenumbraIpcProviders : IDisposable internal readonly FuncProvider< string, int, PenumbraApiEc > RemoveTemporaryModAll; internal readonly FuncProvider< string, string, int, PenumbraApiEc > RemoveTemporaryMod; - public PenumbraIpcProviders( DalamudPluginInterface pi, IPenumbraApi api, Mod.Manager modManager ) + public PenumbraIpcProviders( DalamudPluginInterface pi, IPenumbraApi api, ModManager modManager ) { Api = api; diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 1d571468..f685e886 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -1,422 +1,419 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using OtterGui; -using Penumbra.Mods; -using Penumbra.UI; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Dalamud.Interface.Internal.Notifications; -using Penumbra.GameData.Actors; -using Penumbra.Services; -using Penumbra.Util; - -namespace Penumbra.Collections; - -public partial class ModCollection -{ - public sealed partial class Manager : ISavable - { - public const int Version = 1; - - // The collection currently selected for changing settings. - public ModCollection Current { get; private set; } = Empty; - - // The collection currently selected is in use either as an active collection or through inheritance. - public bool CurrentCollectionInUse { get; private set; } - - // The collection used for general file redirections and all characters not specifically named. - public ModCollection Default { get; private set; } = Empty; - - // The collection used for all files categorized as UI files. - public ModCollection Interface { get; private set; } = Empty; - - // A single collection that can not be deleted as a fallback for the current collection. - private ModCollection DefaultName { get; set; } = Empty; - - // The list of character collections. - public readonly IndividualCollections Individuals; - - public ModCollection Individual(ActorIdentifier identifier) - => Individuals.TryGetCollection(identifier, out var c) ? c : Default; - - // Special Collections - private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues().Length - 3]; - - // Return the configured collection for the given type or null. - // Does not handle Inactive, use ByName instead. - public ModCollection? ByType(CollectionType type) - => ByType(type, ActorIdentifier.Invalid); - - public ModCollection? ByType(CollectionType type, ActorIdentifier identifier) - { - if (type.IsSpecial()) - return _specialCollections[(int)type]; - - return type switch - { - CollectionType.Default => Default, - CollectionType.Interface => Interface, - CollectionType.Current => Current, - CollectionType.Individual => identifier.IsValid && Individuals.Individuals.TryGetValue(identifier, out var c) ? c : null, - _ => null, - }; - } - - // Set a active collection, can be used to set Default, Current, Interface, Special, or Individual collections. - private void SetCollection(int newIdx, CollectionType collectionType, int individualIndex = -1) - { - var oldCollectionIdx = collectionType switch - { - CollectionType.Default => Default.Index, - CollectionType.Interface => Interface.Index, - CollectionType.Current => Current.Index, - CollectionType.Individual => individualIndex < 0 || individualIndex >= Individuals.Count - ? -1 - : Individuals[individualIndex].Collection.Index, - _ when collectionType.IsSpecial() => _specialCollections[(int)collectionType]?.Index ?? Default.Index, - _ => -1, - }; - - if (oldCollectionIdx == -1 || newIdx == oldCollectionIdx) - return; - - var newCollection = this[newIdx]; - if (newIdx > Empty.Index) - newCollection.CreateCache(collectionType is CollectionType.Default); - - switch (collectionType) - { - case CollectionType.Default: - Default = newCollection; - break; - case CollectionType.Interface: - Interface = newCollection; - break; - case CollectionType.Current: - Current = newCollection; - break; - case CollectionType.Individual: - if (!Individuals.ChangeCollection(individualIndex, newCollection)) - { - RemoveCache(newIdx); - return; - } - - break; - default: - _specialCollections[(int)collectionType] = newCollection; - break; - } - - RemoveCache(oldCollectionIdx); - - UpdateCurrentCollectionInUse(); - _communicator.CollectionChange.Invoke(collectionType, this[oldCollectionIdx], newCollection, - collectionType == CollectionType.Individual ? Individuals[individualIndex].DisplayName : string.Empty); - } - - private void UpdateCurrentCollectionInUse() - => CurrentCollectionInUse = _specialCollections - .OfType() - .Prepend(Interface) - .Prepend(Default) - .Concat(Individuals.Assignments.Select(kvp => kvp.Collection)) - .SelectMany(c => c.GetFlattenedInheritance()).Contains(Current); - - public void SetCollection(ModCollection collection, CollectionType collectionType, int individualIndex = -1) - => SetCollection(collection.Index, collectionType, individualIndex); - - // Create a special collection if it does not exist and set it to Empty. - public bool CreateSpecialCollection(CollectionType collectionType) - { - if (!collectionType.IsSpecial() || _specialCollections[(int)collectionType] != null) - return false; - - _specialCollections[(int)collectionType] = Default; - _communicator.CollectionChange.Invoke(collectionType, null, Default, string.Empty); - return true; - } - - // Remove a special collection if it exists - public void RemoveSpecialCollection(CollectionType collectionType) - { - if (!collectionType.IsSpecial()) - return; - - var old = _specialCollections[(int)collectionType]; - if (old != null) - { - _specialCollections[(int)collectionType] = null; - _communicator.CollectionChange.Invoke(collectionType, old, null, string.Empty); - } - } - - // Wrappers around Individual Collection handling. - public void CreateIndividualCollection(params ActorIdentifier[] identifiers) - { - if (Individuals.Add(identifiers, Default)) - _communicator.CollectionChange.Invoke(CollectionType.Individual, null, Default, Individuals.Last().DisplayName); - } - - public void RemoveIndividualCollection(int individualIndex) - { - if (individualIndex < 0 || individualIndex >= Individuals.Count) - return; - - var (name, old) = Individuals[individualIndex]; - if (Individuals.Delete(individualIndex)) - _communicator.CollectionChange.Invoke(CollectionType.Individual, old, null, name); - } - - public void MoveIndividualCollection(int from, int to) - { - if (Individuals.Move(from, to)) - Penumbra.SaveService.QueueSave(this); - } - - // Obtain the index of a collection by name. - private int GetIndexForCollectionName(string name) - => name.Length == 0 ? Empty.Index : _collections.IndexOf(c => c.Name == name); - - // Load default, current, special, and character collections from config. - // Then create caches. If a collection does not exist anymore, reset it to an appropriate default. - private void LoadCollections(FilenameService files) - { - var configChanged = !ReadActiveCollections(files, out var jObject); - - // Load the default collection. - var defaultName = jObject[nameof(Default)]?.ToObject() ?? (configChanged ? DefaultCollection : Empty.Name); - var defaultIdx = GetIndexForCollectionName(defaultName); - if (defaultIdx < 0) - { - Penumbra.ChatService.NotificationMessage( - $"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {Empty.Name}.", "Load Failure", - NotificationType.Warning); - Default = Empty; - configChanged = true; - } - else - { - Default = this[defaultIdx]; - } - - // Load the interface collection. - var interfaceName = jObject[nameof(Interface)]?.ToObject() ?? Default.Name; - var interfaceIdx = GetIndexForCollectionName(interfaceName); - if (interfaceIdx < 0) - { - Penumbra.ChatService.NotificationMessage( - $"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {Empty.Name}.", - "Load Failure", NotificationType.Warning); - Interface = Empty; - configChanged = true; - } - else - { - Interface = this[interfaceIdx]; - } - - // Load the current collection. - var currentName = jObject[nameof(Current)]?.ToObject() ?? DefaultCollection; - var currentIdx = GetIndexForCollectionName(currentName); - if (currentIdx < 0) - { - Penumbra.ChatService.NotificationMessage( - $"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {DefaultCollection}.", - "Load Failure", NotificationType.Warning); - Current = DefaultName; - configChanged = true; - } - else - { - Current = this[currentIdx]; - } - - // Load special collections. - foreach (var (type, name, _) in CollectionTypeExtensions.Special) - { - var typeName = jObject[type.ToString()]?.ToObject(); - if (typeName != null) - { - var idx = GetIndexForCollectionName(typeName); - if (idx < 0) - { - Penumbra.ChatService.NotificationMessage($"Last choice of {name} Collection {typeName} is not available, removed.", - "Load Failure", - NotificationType.Warning); - configChanged = true; - } - else - { - _specialCollections[(int)type] = this[idx]; - } - } - } - - configChanged |= MigrateIndividualCollections(jObject); - configChanged |= Individuals.ReadJObject(jObject[nameof(Individuals)] as JArray, this); - - // Save any changes and create all required caches. - if (configChanged) - Penumbra.SaveService.ImmediateSave(this); - } - - // Migrate ungendered collections to Male and Female for 0.5.9.0. - public static void MigrateUngenderedCollections(FilenameService fileNames) - { - if (!ReadActiveCollections(fileNames, out var jObject)) - return; - - foreach (var (type, _, _) in CollectionTypeExtensions.Special.Where(t => t.Item2.StartsWith("Male "))) - { - var oldName = type.ToString()[4..]; - var value = jObject[oldName]; - if (value == null) - continue; - - jObject.Remove(oldName); - jObject.Add("Male" + oldName, value); - jObject.Add("Female" + oldName, value); - } - - using var stream = File.Open(fileNames.ActiveCollectionsFile, FileMode.Truncate); - using var writer = new StreamWriter(stream); - using var j = new JsonTextWriter(writer); - j.Formatting = Formatting.Indented; - jObject.WriteTo(j); - } - - // Migrate individual collections to Identifiers for 0.6.0. - private bool MigrateIndividualCollections(JObject jObject) - { - var version = jObject[nameof(Version)]?.Value() ?? 0; - if (version > 0) - return false; - - // Load character collections. If a player name comes up multiple times, the last one is applied. - var characters = jObject["Characters"]?.ToObject>() ?? new Dictionary(); - var dict = new Dictionary(characters.Count); - foreach (var (player, collectionName) in characters) - { - var idx = GetIndexForCollectionName(collectionName); - if (idx < 0) - { - Penumbra.ChatService.NotificationMessage( - $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {Empty.Name}.", "Load Failure", - NotificationType.Warning); - dict.Add(player, Empty); - } - else - { - dict.Add(player, this[idx]); - } - } - - Individuals.Migrate0To1(dict); - return true; - } - - // Read the active collection file into a jObject. - // Returns true if this is successful, false if the file does not exist or it is unsuccessful. - private static bool ReadActiveCollections(FilenameService files, out JObject ret) - { - var file = files.ActiveCollectionsFile; - if (File.Exists(file)) - try - { - ret = JObject.Parse(File.ReadAllText(file)); - return true; - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not read active collections from file {file}:\n{e}"); - } - - ret = new JObject(); - return false; - } - - // Save if any of the active collections is changed. - private void SaveOnChange(CollectionType collectionType, ModCollection? _1, ModCollection? _2, string _3) - { - if (collectionType is not CollectionType.Inactive and not CollectionType.Temporary) - Penumbra.SaveService.QueueSave(this); - } - - // Cache handling. Usually recreate caches on the next framework tick, - // but at launch create all of them at once. - public void CreateNecessaryCaches() - { - var tasks = _specialCollections.OfType() - .Concat(Individuals.Select(p => p.Collection)) - .Prepend(Current) - .Prepend(Default) - .Prepend(Interface) - .Distinct() - .Select(c => Task.Run(() => c.CalculateEffectiveFileListInternal(c == Default))) - .ToArray(); - - Task.WaitAll(tasks); - } - - private void RemoveCache(int idx) - { - if (idx != Empty.Index - && idx != Default.Index - && idx != Interface.Index - && idx != Current.Index - && _specialCollections.All(c => c == null || c.Index != idx) - && Individuals.Select(p => p.Collection).All(c => c.Index != idx)) - _collections[idx].ClearCache(); - } - - // Recalculate effective files for active collections on events. - private void OnModAddedActive(Mod mod) - { - foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) - collection._cache!.AddMod(mod, true); - } - - private void OnModRemovedActive(Mod mod) - { - foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) - collection._cache!.RemoveMod(mod, true); - } - - private void OnModMovedActive(Mod mod) - { - foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) - collection._cache!.ReloadMod(mod, true); - } - - public string ToFilename(FilenameService fileNames) - => fileNames.ActiveCollectionsFile; - - public string TypeName - => "Active Collections"; - - public string LogName(string _) - => "to file"; - - public void Save(StreamWriter writer) - { - var jObj = new JObject - { - { nameof(Version), Version }, - { nameof(Default), Default.Name }, - { nameof(Interface), Interface.Name }, - { nameof(Current), Current.Name }, - }; - foreach (var (type, collection) in _specialCollections.WithIndex().Where(p => p.Value != null) - .Select(p => ((CollectionType)p.Index, p.Value!))) - jObj.Add(type.ToString(), collection.Name); - - jObj.Add(nameof(Individuals), Individuals.ToJObject()); - using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; - jObj.WriteTo(j); - } - } -} +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OtterGui; +using Penumbra.Mods; +using Penumbra.UI; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Dalamud.Interface.Internal.Notifications; +using Penumbra.GameData.Actors; +using Penumbra.Services; +using Penumbra.Util; + +namespace Penumbra.Collections; + +public sealed partial class CollectionManager : ISavable +{ + public const int Version = 1; + + // The collection currently selected for changing settings. + public ModCollection Current { get; private set; } = ModCollection.Empty; + + // The collection currently selected is in use either as an active collection or through inheritance. + public bool CurrentCollectionInUse { get; private set; } + + // The collection used for general file redirections and all characters not specifically named. + public ModCollection Default { get; private set; } = ModCollection.Empty; + + // The collection used for all files categorized as UI files. + public ModCollection Interface { get; private set; } = ModCollection.Empty; + + // A single collection that can not be deleted as a fallback for the current collection. + private ModCollection DefaultName { get; set; } = ModCollection.Empty; + + // The list of character collections. + public readonly IndividualCollections Individuals; + + public ModCollection Individual(ActorIdentifier identifier) + => Individuals.TryGetCollection(identifier, out var c) ? c : Default; + + // Special Collections + private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues().Length - 3]; + + // Return the configured collection for the given type or null. + // Does not handle Inactive, use ByName instead. + public ModCollection? ByType(CollectionType type) + => ByType(type, ActorIdentifier.Invalid); + + public ModCollection? ByType(CollectionType type, ActorIdentifier identifier) + { + if (type.IsSpecial()) + return _specialCollections[(int)type]; + + return type switch + { + CollectionType.Default => Default, + CollectionType.Interface => Interface, + CollectionType.Current => Current, + CollectionType.Individual => identifier.IsValid && Individuals.Individuals.TryGetValue(identifier, out var c) ? c : null, + _ => null, + }; + } + + // Set a active collection, can be used to set Default, Current, Interface, Special, or Individual collections. + private void SetCollection(int newIdx, CollectionType collectionType, int individualIndex = -1) + { + var oldCollectionIdx = collectionType switch + { + CollectionType.Default => Default.Index, + CollectionType.Interface => Interface.Index, + CollectionType.Current => Current.Index, + CollectionType.Individual => individualIndex < 0 || individualIndex >= Individuals.Count + ? -1 + : Individuals[individualIndex].Collection.Index, + _ when collectionType.IsSpecial() => _specialCollections[(int)collectionType]?.Index ?? Default.Index, + _ => -1, + }; + + if (oldCollectionIdx == -1 || newIdx == oldCollectionIdx) + return; + + var newCollection = this[newIdx]; + if (newIdx > ModCollection.Empty.Index) + newCollection.CreateCache(collectionType is CollectionType.Default); + + switch (collectionType) + { + case CollectionType.Default: + Default = newCollection; + break; + case CollectionType.Interface: + Interface = newCollection; + break; + case CollectionType.Current: + Current = newCollection; + break; + case CollectionType.Individual: + if (!Individuals.ChangeCollection(individualIndex, newCollection)) + { + RemoveCache(newIdx); + return; + } + + break; + default: + _specialCollections[(int)collectionType] = newCollection; + break; + } + + RemoveCache(oldCollectionIdx); + + UpdateCurrentCollectionInUse(); + _communicator.CollectionChange.Invoke(collectionType, this[oldCollectionIdx], newCollection, + collectionType == CollectionType.Individual ? Individuals[individualIndex].DisplayName : string.Empty); + } + + private void UpdateCurrentCollectionInUse() + => CurrentCollectionInUse = _specialCollections + .OfType() + .Prepend(Interface) + .Prepend(Default) + .Concat(Individuals.Assignments.Select(kvp => kvp.Collection)) + .SelectMany(c => c.GetFlattenedInheritance()).Contains(Current); + + public void SetCollection(ModCollection collection, CollectionType collectionType, int individualIndex = -1) + => SetCollection(collection.Index, collectionType, individualIndex); + + // Create a special collection if it does not exist and set it to Empty. + public bool CreateSpecialCollection(CollectionType collectionType) + { + if (!collectionType.IsSpecial() || _specialCollections[(int)collectionType] != null) + return false; + + _specialCollections[(int)collectionType] = Default; + _communicator.CollectionChange.Invoke(collectionType, null, Default, string.Empty); + return true; + } + + // Remove a special collection if it exists + public void RemoveSpecialCollection(CollectionType collectionType) + { + if (!collectionType.IsSpecial()) + return; + + var old = _specialCollections[(int)collectionType]; + if (old != null) + { + _specialCollections[(int)collectionType] = null; + _communicator.CollectionChange.Invoke(collectionType, old, null, string.Empty); + } + } + + // Wrappers around Individual Collection handling. + public void CreateIndividualCollection(params ActorIdentifier[] identifiers) + { + if (Individuals.Add(identifiers, Default)) + _communicator.CollectionChange.Invoke(CollectionType.Individual, null, Default, Individuals.Last().DisplayName); + } + + public void RemoveIndividualCollection(int individualIndex) + { + if (individualIndex < 0 || individualIndex >= Individuals.Count) + return; + + var (name, old) = Individuals[individualIndex]; + if (Individuals.Delete(individualIndex)) + _communicator.CollectionChange.Invoke(CollectionType.Individual, old, null, name); + } + + public void MoveIndividualCollection(int from, int to) + { + if (Individuals.Move(from, to)) + Penumbra.SaveService.QueueSave(this); + } + + // Obtain the index of a collection by name. + private int GetIndexForCollectionName(string name) + => name.Length == 0 ? ModCollection.Empty.Index : _collections.IndexOf(c => c.Name == name); + + // Load default, current, special, and character collections from config. + // Then create caches. If a collection does not exist anymore, reset it to an appropriate default. + private void LoadCollections(FilenameService files) + { + var configChanged = !ReadActiveCollections(files, out var jObject); + + // Load the default collection. + var defaultName = jObject[nameof(Default)]?.ToObject() ?? (configChanged ? ModCollection.DefaultCollection : ModCollection.Empty.Name); + var defaultIdx = GetIndexForCollectionName(defaultName); + if (defaultIdx < 0) + { + Penumbra.ChatService.NotificationMessage( + $"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Name}.", "Load Failure", + NotificationType.Warning); + Default = ModCollection.Empty; + configChanged = true; + } + else + { + Default = this[defaultIdx]; + } + + // Load the interface collection. + var interfaceName = jObject[nameof(Interface)]?.ToObject() ?? Default.Name; + var interfaceIdx = GetIndexForCollectionName(interfaceName); + if (interfaceIdx < 0) + { + Penumbra.ChatService.NotificationMessage( + $"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Name}.", + "Load Failure", NotificationType.Warning); + Interface = ModCollection.Empty; + configChanged = true; + } + else + { + Interface = this[interfaceIdx]; + } + + // Load the current collection. + var currentName = jObject[nameof(Current)]?.ToObject() ?? ModCollection.DefaultCollection; + var currentIdx = GetIndexForCollectionName(currentName); + if (currentIdx < 0) + { + Penumbra.ChatService.NotificationMessage( + $"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollection.DefaultCollection}.", + "Load Failure", NotificationType.Warning); + Current = DefaultName; + configChanged = true; + } + else + { + Current = this[currentIdx]; + } + + // Load special collections. + foreach (var (type, name, _) in CollectionTypeExtensions.Special) + { + var typeName = jObject[type.ToString()]?.ToObject(); + if (typeName != null) + { + var idx = GetIndexForCollectionName(typeName); + if (idx < 0) + { + Penumbra.ChatService.NotificationMessage($"Last choice of {name} Collection {typeName} is not available, removed.", + "Load Failure", + NotificationType.Warning); + configChanged = true; + } + else + { + _specialCollections[(int)type] = this[idx]; + } + } + } + + configChanged |= MigrateIndividualCollections(jObject); + configChanged |= Individuals.ReadJObject(jObject[nameof(Individuals)] as JArray, this); + + // Save any changes and create all required caches. + if (configChanged) + Penumbra.SaveService.ImmediateSave(this); + } + + // Migrate ungendered collections to Male and Female for 0.5.9.0. + public static void MigrateUngenderedCollections(FilenameService fileNames) + { + if (!ReadActiveCollections(fileNames, out var jObject)) + return; + + foreach (var (type, _, _) in CollectionTypeExtensions.Special.Where(t => t.Item2.StartsWith("Male "))) + { + var oldName = type.ToString()[4..]; + var value = jObject[oldName]; + if (value == null) + continue; + + jObject.Remove(oldName); + jObject.Add("Male" + oldName, value); + jObject.Add("Female" + oldName, value); + } + + using var stream = File.Open(fileNames.ActiveCollectionsFile, FileMode.Truncate); + using var writer = new StreamWriter(stream); + using var j = new JsonTextWriter(writer); + j.Formatting = Formatting.Indented; + jObject.WriteTo(j); + } + + // Migrate individual collections to Identifiers for 0.6.0. + private bool MigrateIndividualCollections(JObject jObject) + { + var version = jObject[nameof(Version)]?.Value() ?? 0; + if (version > 0) + return false; + + // Load character collections. If a player name comes up multiple times, the last one is applied. + var characters = jObject["Characters"]?.ToObject>() ?? new Dictionary(); + var dict = new Dictionary(characters.Count); + foreach (var (player, collectionName) in characters) + { + var idx = GetIndexForCollectionName(collectionName); + if (idx < 0) + { + Penumbra.ChatService.NotificationMessage( + $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {ModCollection.Empty.Name}.", "Load Failure", + NotificationType.Warning); + dict.Add(player, ModCollection.Empty); + } + else + { + dict.Add(player, this[idx]); + } + } + + Individuals.Migrate0To1(dict); + return true; + } + + // Read the active collection file into a jObject. + // Returns true if this is successful, false if the file does not exist or it is unsuccessful. + private static bool ReadActiveCollections(FilenameService files, out JObject ret) + { + var file = files.ActiveCollectionsFile; + if (File.Exists(file)) + try + { + ret = JObject.Parse(File.ReadAllText(file)); + return true; + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not read active collections from file {file}:\n{e}"); + } + + ret = new JObject(); + return false; + } + + // Save if any of the active collections is changed. + private void SaveOnChange(CollectionType collectionType, ModCollection? _1, ModCollection? _2, string _3) + { + if (collectionType is not CollectionType.Inactive and not CollectionType.Temporary) + Penumbra.SaveService.QueueSave(this); + } + + // Cache handling. Usually recreate caches on the next framework tick, + // but at launch create all of them at once. + public void CreateNecessaryCaches() + { + var tasks = _specialCollections.OfType() + .Concat(Individuals.Select(p => p.Collection)) + .Prepend(Current) + .Prepend(Default) + .Prepend(Interface) + .Distinct() + .Select(c => Task.Run(() => c.CalculateEffectiveFileListInternal(c == Default))) + .ToArray(); + + Task.WaitAll(tasks); + } + + private void RemoveCache(int idx) + { + if (idx != ModCollection.Empty.Index + && idx != Default.Index + && idx != Interface.Index + && idx != Current.Index + && _specialCollections.All(c => c == null || c.Index != idx) + && Individuals.Select(p => p.Collection).All(c => c.Index != idx)) + _collections[idx].ClearCache(); + } + + // Recalculate effective files for active collections on events. + private void OnModAddedActive(Mod mod) + { + foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) + collection._cache!.AddMod(mod, true); + } + + private void OnModRemovedActive(Mod mod) + { + foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) + collection._cache!.RemoveMod(mod, true); + } + + private void OnModMovedActive(Mod mod) + { + foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) + collection._cache!.ReloadMod(mod, true); + } + + public string ToFilename(FilenameService fileNames) + => fileNames.ActiveCollectionsFile; + + public string TypeName + => "Active Collections"; + + public string LogName(string _) + => "to file"; + + public void Save(StreamWriter writer) + { + var jObj = new JObject + { + { nameof(Version), Version }, + { nameof(Default), Default.Name }, + { nameof(Interface), Interface.Name }, + { nameof(Current), Current.Name }, + }; + foreach (var (type, collection) in _specialCollections.WithIndex().Where(p => p.Value != null) + .Select(p => ((CollectionType)p.Index, p.Value!))) + jObj.Add(type.ToString(), collection.Name); + + jObj.Add(nameof(Individuals), Individuals.ToJObject()); + using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + jObj.WriteTo(j); + } +} \ No newline at end of file diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 4b883dd3..d03d3ddf 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -17,462 +17,459 @@ using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; namespace Penumbra.Collections; -public partial class ModCollection +public sealed partial class CollectionManager : IDisposable, IEnumerable { - public sealed partial class Manager : IDisposable, IEnumerable + private readonly Mods.ModManager _modManager; + private readonly CommunicatorService _communicator; + private readonly CharacterUtility _characterUtility; + private readonly ResidentResourceManager _residentResources; + private readonly Configuration _config; + + + // The empty collection is always available and always has index 0. + // It can not be deleted or moved. + private readonly List _collections = new() { - private readonly Mod.Manager _modManager; - private readonly CommunicatorService _communicator; - private readonly CharacterUtility _characterUtility; - private readonly ResidentResourceManager _residentResources; - private readonly Configuration _config; + ModCollection.Empty, + }; + public ModCollection this[Index idx] + => _collections[idx]; - // The empty collection is always available and always has index 0. - // It can not be deleted or moved. - private readonly List _collections = new() + public ModCollection? this[string name] + => ByName(name, out var c) ? c : null; + + public int Count + => _collections.Count; + + // Obtain a collection case-independently by name. + public bool ByName(string name, [NotNullWhen(true)] out ModCollection? collection) + => _collections.FindFirst(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase), out collection); + + // Default enumeration skips the empty collection. + public IEnumerator GetEnumerator() + => _collections.Skip(1).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public IEnumerable GetEnumeratorWithEmpty() + => _collections; + + public CollectionManager(StartTracker timer, CommunicatorService communicator, FilenameService files, CharacterUtility characterUtility, + ResidentResourceManager residentResources, Configuration config, Mods.ModManager modManager, IndividualCollections individuals) + { + using var time = timer.Measure(StartTimeType.Collections); + _communicator = communicator; + _characterUtility = characterUtility; + _residentResources = residentResources; + _config = config; + _modManager = modManager; + Individuals = individuals; + + // The collection manager reacts to changes in mods by itself. + _modManager.ModDiscoveryStarted += OnModDiscoveryStarted; + _modManager.ModDiscoveryFinished += OnModDiscoveryFinished; + _modManager.ModOptionChanged += OnModOptionsChanged; + _modManager.ModPathChanged += OnModPathChange; + _communicator.CollectionChange.Event += SaveOnChange; + _communicator.TemporaryGlobalModChange.Event += OnGlobalModChange; + ReadCollections(files); + LoadCollections(files); + UpdateCurrentCollectionInUse(); + CreateNecessaryCaches(); + } + + public void Dispose() + { + _communicator.CollectionChange.Event -= SaveOnChange; + _communicator.TemporaryGlobalModChange.Event -= OnGlobalModChange; + _modManager.ModDiscoveryStarted -= OnModDiscoveryStarted; + _modManager.ModDiscoveryFinished -= OnModDiscoveryFinished; + _modManager.ModOptionChanged -= OnModOptionsChanged; + _modManager.ModPathChanged -= OnModPathChange; + } + + private void OnGlobalModChange(TemporaryMod mod, bool created, bool removed) + => TempModManager.OnGlobalModChange(_collections, mod, created, removed); + + // Returns true if the name is not empty, it is not the name of the empty collection + // and no existing collection results in the same filename as name. + public bool CanAddCollection(string name, out string fixedName) + { + if (!ModCollection.IsValidName(name)) { - Empty, - }; - - public ModCollection this[Index idx] - => _collections[idx]; - - public ModCollection? this[string name] - => ByName(name, out var c) ? c : null; - - public int Count - => _collections.Count; - - // Obtain a collection case-independently by name. - public bool ByName(string name, [NotNullWhen(true)] out ModCollection? collection) - => _collections.FindFirst(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase), out collection); - - // Default enumeration skips the empty collection. - public IEnumerator GetEnumerator() - => _collections.Skip(1).GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public IEnumerable GetEnumeratorWithEmpty() - => _collections; - - public Manager(StartTracker timer, CommunicatorService communicator, FilenameService files, CharacterUtility characterUtility, - ResidentResourceManager residentResources, Configuration config, Mod.Manager manager, IndividualCollections individuals) - { - using var time = timer.Measure(StartTimeType.Collections); - _communicator = communicator; - _characterUtility = characterUtility; - _residentResources = residentResources; - _config = config; - _modManager = manager; - Individuals = individuals; - - // The collection manager reacts to changes in mods by itself. - _modManager.ModDiscoveryStarted += OnModDiscoveryStarted; - _modManager.ModDiscoveryFinished += OnModDiscoveryFinished; - _modManager.ModOptionChanged += OnModOptionsChanged; - _modManager.ModPathChanged += OnModPathChange; - _communicator.CollectionChange.Event += SaveOnChange; - _communicator.TemporaryGlobalModChange.Event += OnGlobalModChange; - ReadCollections(files); - LoadCollections(files); - UpdateCurrentCollectionInUse(); - CreateNecessaryCaches(); + fixedName = string.Empty; + return false; } - public void Dispose() + name = name.RemoveInvalidPathSymbols().ToLowerInvariant(); + if (name.Length == 0 + || name == ModCollection.Empty.Name.ToLowerInvariant() + || _collections.Any(c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == name)) { - _communicator.CollectionChange.Event -= SaveOnChange; - _communicator.TemporaryGlobalModChange.Event -= OnGlobalModChange; - _modManager.ModDiscoveryStarted -= OnModDiscoveryStarted; - _modManager.ModDiscoveryFinished -= OnModDiscoveryFinished; - _modManager.ModOptionChanged -= OnModOptionsChanged; - _modManager.ModPathChanged -= OnModPathChange; + fixedName = string.Empty; + return false; } - private void OnGlobalModChange(TemporaryMod mod, bool created, bool removed) - => TempModManager.OnGlobalModChange(_collections, mod, created, removed); + fixedName = name; + return true; + } - // Returns true if the name is not empty, it is not the name of the empty collection - // and no existing collection results in the same filename as name. - public bool CanAddCollection(string name, out string fixedName) + // Add a new collection of the given name. + // If duplicate is not-null, the new collection will be a duplicate of it. + // If the name of the collection would result in an already existing filename, skip it. + // Returns true if the collection was successfully created and fires a Inactive event. + // Also sets the current collection to the new collection afterwards. + public bool AddCollection(string name, ModCollection? duplicate) + { + if (!CanAddCollection(name, out var fixedName)) { - if (!IsValidName(name)) - { - fixedName = string.Empty; - return false; - } - - name = name.RemoveInvalidPathSymbols().ToLowerInvariant(); - if (name.Length == 0 - || name == Empty.Name.ToLowerInvariant() - || _collections.Any(c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == name)) - { - fixedName = string.Empty; - return false; - } - - fixedName = name; - return true; + Penumbra.Log.Warning($"The new collection {name} would lead to the same path {fixedName} as one that already exists."); + return false; } - // Add a new collection of the given name. - // If duplicate is not-null, the new collection will be a duplicate of it. - // If the name of the collection would result in an already existing filename, skip it. - // Returns true if the collection was successfully created and fires a Inactive event. - // Also sets the current collection to the new collection afterwards. - public bool AddCollection(string name, ModCollection? duplicate) + var newCollection = duplicate?.Duplicate(name) ?? ModCollection.CreateNewEmpty(name); + newCollection.Index = _collections.Count; + _collections.Add(newCollection); + + Penumbra.SaveService.ImmediateSave(newCollection); + Penumbra.Log.Debug($"Added collection {newCollection.AnonymizedName}."); + _communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty); + SetCollection(newCollection.Index, CollectionType.Current); + return true; + } + + // Remove the given collection if it exists and is neither the empty nor the default-named collection. + // If the removed collection was active, it also sets the corresponding collection to the appropriate default. + // Also removes the collection from inheritances of all other collections. + public bool RemoveCollection(int idx) + { + if (idx <= ModCollection.Empty.Index || idx >= _collections.Count) { - if (!CanAddCollection(name, out var fixedName)) - { - Penumbra.Log.Warning($"The new collection {name} would lead to the same path {fixedName} as one that already exists."); - return false; - } - - var newCollection = duplicate?.Duplicate(name) ?? CreateNewEmpty(name); - newCollection.Index = _collections.Count; - _collections.Add(newCollection); - - Penumbra.SaveService.ImmediateSave(newCollection); - Penumbra.Log.Debug($"Added collection {newCollection.AnonymizedName}."); - _communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty); - SetCollection(newCollection.Index, CollectionType.Current); - return true; + Penumbra.Log.Error("Can not remove the empty collection."); + return false; } - // Remove the given collection if it exists and is neither the empty nor the default-named collection. - // If the removed collection was active, it also sets the corresponding collection to the appropriate default. - // Also removes the collection from inheritances of all other collections. - public bool RemoveCollection(int idx) + if (idx == DefaultName.Index) { - if (idx <= Empty.Index || idx >= _collections.Count) - { - Penumbra.Log.Error("Can not remove the empty collection."); - return false; - } - - if (idx == DefaultName.Index) - { - Penumbra.Log.Error("Can not remove the default collection."); - return false; - } - - if (idx == Current.Index) - SetCollection(DefaultName.Index, CollectionType.Current); - - if (idx == Default.Index) - SetCollection(Empty.Index, CollectionType.Default); - - for (var i = 0; i < _specialCollections.Length; ++i) - { - if (idx == _specialCollections[i]?.Index) - SetCollection(Empty, (CollectionType)i); - } - - for (var i = 0; i < Individuals.Count; ++i) - { - if (Individuals[i].Collection.Index == idx) - SetCollection(Empty, CollectionType.Individual, i); - } - - var collection = _collections[idx]; - - // Clear own inheritances. - foreach (var inheritance in collection.Inheritance) - collection.ClearSubscriptions(inheritance); - - Penumbra.SaveService.ImmediateDelete(collection); - _collections.RemoveAt(idx); - - // Clear external inheritances. - foreach (var c in _collections) - { - var inheritedIdx = c._inheritance.IndexOf(collection); - if (inheritedIdx >= 0) - c.RemoveInheritance(inheritedIdx); - - if (c.Index > idx) - --c.Index; - } - - Penumbra.Log.Debug($"Removed collection {collection.AnonymizedName}."); - _communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty); - return true; + Penumbra.Log.Error("Can not remove the default collection."); + return false; } - public bool RemoveCollection(ModCollection collection) - => RemoveCollection(collection.Index); + if (idx == Current.Index) + SetCollection(DefaultName.Index, CollectionType.Current); - private void OnModDiscoveryStarted() + if (idx == Default.Index) + SetCollection(ModCollection.Empty.Index, CollectionType.Default); + + for (var i = 0; i < _specialCollections.Length; ++i) { - foreach (var collection in this) - collection.PrepareModDiscovery(); + if (idx == _specialCollections[i]?.Index) + SetCollection(ModCollection.Empty, (CollectionType)i); } - private void OnModDiscoveryFinished() + for (var i = 0; i < Individuals.Count; ++i) { - // First, re-apply all mod settings. - foreach (var collection in this) - collection.ApplyModSettings(); - - // Afterwards, we update the caches. This can not happen in the same loop due to inheritance. - foreach (var collection in this.Where(c => c.HasCache)) - collection.ForceCacheUpdate(); + if (Individuals[i].Collection.Index == idx) + SetCollection(ModCollection.Empty, CollectionType.Individual, i); } + var collection = _collections[idx]; - // A changed mod path forces changes for all collections, active and inactive. - private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, - DirectoryInfo? newDirectory) + // Clear own inheritances. + foreach (var inheritance in collection.Inheritance) + collection.ClearSubscriptions(inheritance); + + Penumbra.SaveService.ImmediateDelete(collection); + _collections.RemoveAt(idx); + + // Clear external inheritances. + foreach (var c in _collections) { - switch (type) - { - case ModPathChangeType.Added: - foreach (var collection in this) - collection.AddMod(mod); + var inheritedIdx = c._inheritance.IndexOf(collection); + if (inheritedIdx >= 0) + c.RemoveInheritance(inheritedIdx); - OnModAddedActive(mod); - break; - case ModPathChangeType.Deleted: - OnModRemovedActive(mod); - foreach (var collection in this) - collection.RemoveMod(mod, mod.Index); - - break; - case ModPathChangeType.Moved: - OnModMovedActive(mod); - foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null)) - Penumbra.SaveService.QueueSave(collection); - - break; - case ModPathChangeType.StartingReload: - OnModRemovedActive(mod); - break; - case ModPathChangeType.Reloaded: - OnModAddedActive(mod); - break; - default: throw new ArgumentOutOfRangeException(nameof(type), type, null); - } + if (c.Index > idx) + --c.Index; } - // Automatically update all relevant collections when a mod is changed. - // This means saving if options change in a way where the settings may change and the collection has settings for this mod. - // And also updating effective file and meta manipulation lists if necessary. - private void OnModOptionsChanged(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx) + Penumbra.Log.Debug($"Removed collection {collection.AnonymizedName}."); + _communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty); + return true; + } + + public bool RemoveCollection(ModCollection collection) + => RemoveCollection(collection.Index); + + private void OnModDiscoveryStarted() + { + foreach (var collection in this) + collection.PrepareModDiscovery(); + } + + private void OnModDiscoveryFinished() + { + // First, re-apply all mod settings. + foreach (var collection in this) + collection.ApplyModSettings(); + + // Afterwards, we update the caches. This can not happen in the same loop due to inheritance. + foreach (var collection in this.Where(c => c.HasCache)) + collection.ForceCacheUpdate(); + } + + + // A changed mod path forces changes for all collections, active and inactive. + private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, + DirectoryInfo? newDirectory) + { + switch (type) { - // Handle changes that break revertability. - if (type == ModOptionChangeType.PrepareChange) - { - foreach (var collection in this.Where(c => c.HasCache)) - { - if (collection[mod.Index].Settings is { Enabled: true }) - collection._cache!.RemoveMod(mod, false); - } - - return; - } - - type.HandlingInfo(out var requiresSaving, out var recomputeList, out var reload); - - // Handle changes that require overwriting the collection. - if (requiresSaving) + case ModPathChangeType.Added: foreach (var collection in this) - { - if (collection._settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false) - Penumbra.SaveService.QueueSave(collection); - } + collection.AddMod(mod); - // Handle changes that reload the mod if the changes did not need to be prepared, - // or re-add the mod if they were prepared. - if (recomputeList) - foreach (var collection in this.Where(c => c.HasCache)) - { - if (collection[mod.Index].Settings is { Enabled: true }) - { - if (reload) - collection._cache!.ReloadMod(mod, true); - else - collection._cache!.AddMod(mod, true); - } - } - } + OnModAddedActive(mod); + break; + case ModPathChangeType.Deleted: + OnModRemovedActive(mod); + foreach (var collection in this) + collection.RemoveMod(mod, mod.Index); - // Add the collection with the default name if it does not exist. - // It should always be ensured that it exists, otherwise it will be created. - // This can also not be deleted, so there are always at least the empty and a collection with default name. - private void AddDefaultCollection() - { - var idx = GetIndexForCollectionName(DefaultCollection); - if (idx >= 0) - { - DefaultName = this[idx]; - return; - } + break; + case ModPathChangeType.Moved: + OnModMovedActive(mod); + foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null)) + Penumbra.SaveService.QueueSave(collection); - var defaultCollection = CreateNewEmpty(DefaultCollection); - Penumbra.SaveService.ImmediateSave(defaultCollection); - defaultCollection.Index = _collections.Count; - _collections.Add(defaultCollection); - } - - // Inheritances can not be setup before all collections are read, - // so this happens after reading the collections. - private void ApplyInheritances(IEnumerable> inheritances) - { - foreach (var (collection, inheritance) in this.Zip(inheritances)) - { - var changes = false; - foreach (var subCollectionName in inheritance) - { - if (!ByName(subCollectionName, out var subCollection)) - { - changes = true; - Penumbra.Log.Warning($"Inherited collection {subCollectionName} for {collection.Name} does not exist, removed."); - } - else if (!collection.AddInheritance(subCollection, false)) - { - changes = true; - Penumbra.Log.Warning($"{collection.Name} can not inherit from {subCollectionName}, removed."); - } - } - - if (changes) - Penumbra.SaveService.ImmediateSave(collection); - } - } - - // Read all collection files in the Collection Directory. - // Ensure that the default named collection exists, and apply inheritances afterwards. - // Duplicate collection files are not deleted, just not added here. - private void ReadCollections(FilenameService files) - { - var inheritances = new List>(); - foreach (var file in files.CollectionFiles) - { - var collection = LoadFromFile(file, out var inheritance); - if (collection == null || collection.Name.Length == 0) - continue; - - if (file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json") - Penumbra.Log.Warning($"Collection {file.Name} does not correspond to {collection.Name}."); - - if (this[collection.Name] != null) - { - Penumbra.Log.Warning($"Duplicate collection found: {collection.Name} already exists."); - } - else - { - inheritances.Add(inheritance); - collection.Index = _collections.Count; - _collections.Add(collection); - } - } - - AddDefaultCollection(); - ApplyInheritances(inheritances); - } - - public string RedundancyCheck(CollectionType type, ActorIdentifier id) - { - var checkAssignment = ByType(type, id); - if (checkAssignment == null) - return string.Empty; - - switch (type) - { - // Check individual assignments. We can only be sure of redundancy for world-overlap or ownership overlap. - case CollectionType.Individual: - switch (id.Type) - { - case IdentifierType.Player when id.HomeWorld != ushort.MaxValue: - { - var global = ByType(CollectionType.Individual, Penumbra.Actors.CreatePlayer(id.PlayerName, ushort.MaxValue)); - return global?.Index == checkAssignment.Index - ? "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it." - : string.Empty; - } - case IdentifierType.Owned: - if (id.HomeWorld != ushort.MaxValue) - { - var global = ByType(CollectionType.Individual, - Penumbra.Actors.CreateOwned(id.PlayerName, ushort.MaxValue, id.Kind, id.DataId)); - if (global?.Index == checkAssignment.Index) - return "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it."; - } - - var unowned = ByType(CollectionType.Individual, Penumbra.Actors.CreateNpc(id.Kind, id.DataId)); - return unowned?.Index == checkAssignment.Index - ? "Assignment is redundant due to an identical unowned NPC assignment existing.\nYou can remove it." - : string.Empty; - } - break; - // The group of all Characters is redundant if they are all equal to Default or unassigned. - case CollectionType.MalePlayerCharacter: - case CollectionType.MaleNonPlayerCharacter: - case CollectionType.FemalePlayerCharacter: - case CollectionType.FemaleNonPlayerCharacter: - var first = ByType(CollectionType.MalePlayerCharacter) ?? Default; - var second = ByType(CollectionType.MaleNonPlayerCharacter) ?? Default; - var third = ByType(CollectionType.FemalePlayerCharacter) ?? Default; - var fourth = ByType(CollectionType.FemaleNonPlayerCharacter) ?? Default; - if (first.Index == second.Index - && first.Index == third.Index - && first.Index == fourth.Index - && first.Index == Default.Index) - return - "Assignment is currently redundant due to the group [Male, Female, Player, NPC] Characters being unassigned or identical to each other and Default.\n" - + "You can keep just the Default Assignment."; - - break; - // Children and Elderly are redundant if they are identical to both Male NPCs and Female NPCs, or if they are unassigned to Default. - case CollectionType.NonPlayerChild: - case CollectionType.NonPlayerElderly: - var maleNpc = ByType(CollectionType.MaleNonPlayerCharacter); - var femaleNpc = ByType(CollectionType.FemaleNonPlayerCharacter); - var collection1 = CollectionType.MaleNonPlayerCharacter; - var collection2 = CollectionType.FemaleNonPlayerCharacter; - if (maleNpc == null) - { - maleNpc = Default; - if (maleNpc.Index != checkAssignment.Index) - return string.Empty; - - collection1 = CollectionType.Default; - } - - if (femaleNpc == null) - { - femaleNpc = Default; - if (femaleNpc.Index != checkAssignment.Index) - return string.Empty; - - collection2 = CollectionType.Default; - } - - return collection1 == collection2 - ? $"Assignment is currently redundant due to overwriting {collection1.ToName()} with an identical collection.\nYou can remove them." - : $"Assignment is currently redundant due to overwriting {collection1.ToName()} and {collection2.ToName()} with an identical collection.\nYou can remove them."; - - // For other assignments, check the inheritance order, unassigned means fall-through, - // assigned needs identical assignments to be redundant. - default: - var group = type.InheritanceOrder(); - foreach (var parentType in group) - { - var assignment = ByType(parentType); - if (assignment == null) - continue; - - if (assignment.Index == checkAssignment.Index) - return $"Assignment is currently redundant due to overwriting {parentType.ToName()} with an identical collection.\nYou can remove it."; - } - - break; - } - - return string.Empty; + break; + case ModPathChangeType.StartingReload: + OnModRemovedActive(mod); + break; + case ModPathChangeType.Reloaded: + OnModAddedActive(mod); + break; + default: throw new ArgumentOutOfRangeException(nameof(type), type, null); } } + + // Automatically update all relevant collections when a mod is changed. + // This means saving if options change in a way where the settings may change and the collection has settings for this mod. + // And also updating effective file and meta manipulation lists if necessary. + private void OnModOptionsChanged(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx) + { + // Handle changes that break revertability. + if (type == ModOptionChangeType.PrepareChange) + { + foreach (var collection in this.Where(c => c.HasCache)) + { + if (collection[mod.Index].Settings is { Enabled: true }) + collection._cache!.RemoveMod(mod, false); + } + + return; + } + + type.HandlingInfo(out var requiresSaving, out var recomputeList, out var reload); + + // Handle changes that require overwriting the collection. + if (requiresSaving) + foreach (var collection in this) + { + if (collection._settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false) + Penumbra.SaveService.QueueSave(collection); + } + + // Handle changes that reload the mod if the changes did not need to be prepared, + // or re-add the mod if they were prepared. + if (recomputeList) + foreach (var collection in this.Where(c => c.HasCache)) + { + if (collection[mod.Index].Settings is { Enabled: true }) + { + if (reload) + collection._cache!.ReloadMod(mod, true); + else + collection._cache!.AddMod(mod, true); + } + } + } + + // Add the collection with the default name if it does not exist. + // It should always be ensured that it exists, otherwise it will be created. + // This can also not be deleted, so there are always at least the empty and a collection with default name. + private void AddDefaultCollection() + { + var idx = GetIndexForCollectionName(ModCollection.DefaultCollection); + if (idx >= 0) + { + DefaultName = this[idx]; + return; + } + + var defaultCollection = ModCollection.CreateNewEmpty((string)ModCollection.DefaultCollection); + Penumbra.SaveService.ImmediateSave(defaultCollection); + defaultCollection.Index = _collections.Count; + _collections.Add(defaultCollection); + } + + // Inheritances can not be setup before all collections are read, + // so this happens after reading the collections. + private void ApplyInheritances(IEnumerable> inheritances) + { + foreach (var (collection, inheritance) in this.Zip(inheritances)) + { + var changes = false; + foreach (var subCollectionName in inheritance) + { + if (!ByName(subCollectionName, out var subCollection)) + { + changes = true; + Penumbra.Log.Warning($"Inherited collection {subCollectionName} for {collection.Name} does not exist, removed."); + } + else if (!collection.AddInheritance(subCollection, false)) + { + changes = true; + Penumbra.Log.Warning($"{collection.Name} can not inherit from {subCollectionName}, removed."); + } + } + + if (changes) + Penumbra.SaveService.ImmediateSave(collection); + } + } + + // Read all collection files in the Collection Directory. + // Ensure that the default named collection exists, and apply inheritances afterwards. + // Duplicate collection files are not deleted, just not added here. + private void ReadCollections(FilenameService files) + { + var inheritances = new List>(); + foreach (var file in files.CollectionFiles) + { + var collection = ModCollection.LoadFromFile(file, out var inheritance); + if (collection == null || collection.Name.Length == 0) + continue; + + if (file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json") + Penumbra.Log.Warning($"Collection {file.Name} does not correspond to {collection.Name}."); + + if (this[collection.Name] != null) + { + Penumbra.Log.Warning($"Duplicate collection found: {collection.Name} already exists."); + } + else + { + inheritances.Add(inheritance); + collection.Index = _collections.Count; + _collections.Add(collection); + } + } + + AddDefaultCollection(); + ApplyInheritances(inheritances); + } + + public string RedundancyCheck(CollectionType type, ActorIdentifier id) + { + var checkAssignment = ByType(type, id); + if (checkAssignment == null) + return string.Empty; + + switch (type) + { + // Check individual assignments. We can only be sure of redundancy for world-overlap or ownership overlap. + case CollectionType.Individual: + switch (id.Type) + { + case IdentifierType.Player when id.HomeWorld != ushort.MaxValue: + { + var global = ByType(CollectionType.Individual, Penumbra.Actors.CreatePlayer(id.PlayerName, ushort.MaxValue)); + return global?.Index == checkAssignment.Index + ? "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it." + : string.Empty; + } + case IdentifierType.Owned: + if (id.HomeWorld != ushort.MaxValue) + { + var global = ByType(CollectionType.Individual, + Penumbra.Actors.CreateOwned(id.PlayerName, ushort.MaxValue, id.Kind, id.DataId)); + if (global?.Index == checkAssignment.Index) + return "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it."; + } + + var unowned = ByType(CollectionType.Individual, Penumbra.Actors.CreateNpc(id.Kind, id.DataId)); + return unowned?.Index == checkAssignment.Index + ? "Assignment is redundant due to an identical unowned NPC assignment existing.\nYou can remove it." + : string.Empty; + } + break; + // The group of all Characters is redundant if they are all equal to Default or unassigned. + case CollectionType.MalePlayerCharacter: + case CollectionType.MaleNonPlayerCharacter: + case CollectionType.FemalePlayerCharacter: + case CollectionType.FemaleNonPlayerCharacter: + var first = ByType(CollectionType.MalePlayerCharacter) ?? Default; + var second = ByType(CollectionType.MaleNonPlayerCharacter) ?? Default; + var third = ByType(CollectionType.FemalePlayerCharacter) ?? Default; + var fourth = ByType(CollectionType.FemaleNonPlayerCharacter) ?? Default; + if (first.Index == second.Index + && first.Index == third.Index + && first.Index == fourth.Index + && first.Index == Default.Index) + return + "Assignment is currently redundant due to the group [Male, Female, Player, NPC] Characters being unassigned or identical to each other and Default.\n" + + "You can keep just the Default Assignment."; + + break; + // Children and Elderly are redundant if they are identical to both Male NPCs and Female NPCs, or if they are unassigned to Default. + case CollectionType.NonPlayerChild: + case CollectionType.NonPlayerElderly: + var maleNpc = ByType(CollectionType.MaleNonPlayerCharacter); + var femaleNpc = ByType(CollectionType.FemaleNonPlayerCharacter); + var collection1 = CollectionType.MaleNonPlayerCharacter; + var collection2 = CollectionType.FemaleNonPlayerCharacter; + if (maleNpc == null) + { + maleNpc = Default; + if (maleNpc.Index != checkAssignment.Index) + return string.Empty; + + collection1 = CollectionType.Default; + } + + if (femaleNpc == null) + { + femaleNpc = Default; + if (femaleNpc.Index != checkAssignment.Index) + return string.Empty; + + collection2 = CollectionType.Default; + } + + return collection1 == collection2 + ? $"Assignment is currently redundant due to overwriting {collection1.ToName()} with an identical collection.\nYou can remove them." + : $"Assignment is currently redundant due to overwriting {collection1.ToName()} and {collection2.ToName()} with an identical collection.\nYou can remove them."; + + // For other assignments, check the inheritance order, unassigned means fall-through, + // assigned needs identical assignments to be redundant. + default: + var group = type.InheritanceOrder(); + foreach (var parentType in group) + { + var assignment = ByType(parentType); + if (assignment == null) + continue; + + if (assignment.Index == checkAssignment.Index) + return $"Assignment is currently redundant due to overwriting {parentType.ToName()} with an identical collection.\nYou can remove it."; + } + + break; + } + + return string.Empty; + } } diff --git a/Penumbra/Collections/IndividualCollections.Files.cs b/Penumbra/Collections/IndividualCollections.Files.cs index 2dd67e3c..498688ed 100644 --- a/Penumbra/Collections/IndividualCollections.Files.cs +++ b/Penumbra/Collections/IndividualCollections.Files.cs @@ -26,7 +26,7 @@ public partial class IndividualCollections return ret; } - public bool ReadJObject( JArray? obj, ModCollection.Manager manager ) + public bool ReadJObject( JArray? obj, CollectionManager manager ) { if( obj == null ) { diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 37bfafd5..355f17b3 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -8,7 +8,6 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; using System.Threading; -using Penumbra.Interop; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; @@ -20,27 +19,27 @@ namespace Penumbra.Collections; public partial class ModCollection { // Only active collections need to have a cache. - private Cache? _cache; + internal ModCollectionCache? _cache; public bool HasCache => _cache != null; // Count the number of changes of the effective file list. // This is used for material and imc changes. - public int ChangeCounter { get; private set; } + public int ChangeCounter { get; internal set; } // Only create, do not update. - private void CreateCache(bool isDefault) + internal void CreateCache(bool isDefault) { - if (_cache == null) - { - CalculateEffectiveFileList(isDefault); - Penumbra.Log.Verbose($"Created new cache for collection {Name}."); - } + if (_cache != null) + return; + + CalculateEffectiveFileList(isDefault); + Penumbra.Log.Verbose($"Created new cache for collection {Name}."); } // Force an update with metadata for this cache. - private void ForceCacheUpdate() + internal void ForceCacheUpdate() => CalculateEffectiveFileList(this == Penumbra.CollectionManager.Default); // Handle temporary mods for this collection. @@ -83,7 +82,7 @@ public partial class ModCollection } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool CheckFullPath(Utf8GamePath path, FullPath fullPath) + internal static bool CheckFullPath(Utf8GamePath path, FullPath fullPath) { if (fullPath.InternalName.Length < Utf8GamePath.MaxGamePathLength) return true; @@ -127,14 +126,14 @@ public partial class ModCollection => Penumbra.Framework.RegisterImportant(nameof(CalculateEffectiveFileList) + Name, () => CalculateEffectiveFileListInternal(isDefault)); - private void CalculateEffectiveFileListInternal(bool isDefault) + internal void CalculateEffectiveFileListInternal(bool isDefault) { // Skip the empty collection. if (Index == 0) return; Penumbra.Log.Debug($"[{Thread.CurrentThread.ManagedThreadId}] Recalculating effective file list for {AnonymizedName}"); - _cache ??= new Cache(this); + _cache ??= new ModCollectionCache(this); _cache.FullRecalculation(isDefault); Penumbra.Log.Debug($"[{Thread.CurrentThread.ManagedThreadId}] Recalculation of effective file list for {AnonymizedName} finished."); diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index cd148344..e25f65bf 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -15,532 +15,531 @@ namespace Penumbra.Collections; public record struct ModPath( IMod Mod, FullPath Path ); public record ModConflicts( IMod Mod2, List< object > Conflicts, bool HasPriority, bool Solved ); -public partial class ModCollection +/// +/// The Cache contains all required temporary data to use a collection. +/// It will only be setup if a collection gets activated in any way. +/// +internal class ModCollectionCache : IDisposable { - // The Cache contains all required temporary data to use a collection. - // It will only be setup if a collection gets activated in any way. - private class Cache : IDisposable + private readonly ModCollection _collection; + private readonly SortedList< string, (SingleArray< IMod >, object?) > _changedItems = new(); + public readonly Dictionary< Utf8GamePath, ModPath > ResolvedFiles = new(); + public readonly MetaManager MetaManipulations; + private readonly Dictionary< IMod, SingleArray< ModConflicts > > _conflicts = new(); + + public IEnumerable< SingleArray< ModConflicts > > AllConflicts + => _conflicts.Values; + + public SingleArray< ModConflicts > Conflicts( IMod mod ) + => _conflicts.TryGetValue( mod, out var c ) ? c : new SingleArray< ModConflicts >(); + + private int _changedItemsSaveCounter = -1; + + // Obtain currently changed items. Computes them if they haven't been computed before. + public IReadOnlyDictionary< string, (SingleArray< IMod >, object?) > ChangedItems { - private readonly ModCollection _collection; - private readonly SortedList< string, (SingleArray< IMod >, object?) > _changedItems = new(); - public readonly Dictionary< Utf8GamePath, ModPath > ResolvedFiles = new(); - public readonly MetaManager MetaManipulations; - private readonly Dictionary< IMod, SingleArray< ModConflicts > > _conflicts = new(); - - public IEnumerable< SingleArray< ModConflicts > > AllConflicts - => _conflicts.Values; - - public SingleArray< ModConflicts > Conflicts( IMod mod ) - => _conflicts.TryGetValue( mod, out var c ) ? c : new SingleArray< ModConflicts >(); - - private int _changedItemsSaveCounter = -1; - - // Obtain currently changed items. Computes them if they haven't been computed before. - public IReadOnlyDictionary< string, (SingleArray< IMod >, object?) > ChangedItems + get { - get + SetChangedItems(); + return _changedItems; + } + } + + // The cache reacts through events on its collection changing. + public ModCollectionCache( ModCollection collection ) + { + _collection = collection; + MetaManipulations = new MetaManager( _collection ); + _collection.ModSettingChanged += OnModSettingChange; + _collection.InheritanceChanged += OnInheritanceChange; + if( !Penumbra.CharacterUtility.Ready ) + { + Penumbra.CharacterUtility.LoadingFinished += IncrementCounter; + } + } + + public void Dispose() + { + MetaManipulations.Dispose(); + _collection.ModSettingChanged -= OnModSettingChange; + _collection.InheritanceChanged -= OnInheritanceChange; + Penumbra.CharacterUtility.LoadingFinished -= IncrementCounter; + } + + // Resolve a given game path according to this collection. + public FullPath? ResolvePath( Utf8GamePath gameResourcePath ) + { + if( !ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ) ) + { + return null; + } + + if( candidate.Path.InternalName.Length > Utf8GamePath.MaxGamePathLength + || candidate.Path.IsRooted && !candidate.Path.Exists ) + { + return null; + } + + return candidate.Path; + } + + // For a given full path, find all game paths that currently use this file. + public IEnumerable< Utf8GamePath > ReverseResolvePath( FullPath localFilePath ) + { + var needle = localFilePath.FullName.ToLower(); + if( localFilePath.IsRooted ) + { + needle = needle.Replace( '/', '\\' ); + } + + var iterator = ResolvedFiles + .Where( f => string.Equals( f.Value.Path.FullName, needle, StringComparison.OrdinalIgnoreCase ) ) + .Select( kvp => kvp.Key ); + + // For files that are not rooted, try to add themselves. + if( !localFilePath.IsRooted && Utf8GamePath.FromString( localFilePath.FullName, out var utf8 ) ) + { + iterator = iterator.Prepend( utf8 ); + } + + return iterator; + } + + // Reverse resolve multiple paths at once for efficiency. + public HashSet< Utf8GamePath >[] ReverseResolvePaths( IReadOnlyCollection< string > fullPaths ) + { + if( fullPaths.Count == 0 ) + return Array.Empty< HashSet< Utf8GamePath > >(); + + var ret = new HashSet< Utf8GamePath >[fullPaths.Count]; + var dict = new Dictionary< FullPath, int >( fullPaths.Count ); + foreach( var (path, idx) in fullPaths.WithIndex() ) + { + dict[ new FullPath(path) ] = idx; + ret[ idx ] = !Path.IsPathRooted( path ) && Utf8GamePath.FromString( path, out var utf8 ) + ? new HashSet< Utf8GamePath > { utf8 } + : new HashSet< Utf8GamePath >(); + } + + foreach( var (game, full) in ResolvedFiles ) + { + if( dict.TryGetValue( full.Path, out var idx ) ) { - SetChangedItems(); - return _changedItems; + ret[ idx ].Add( game ); } } - // The cache reacts through events on its collection changing. - public Cache( ModCollection collection ) + return ret; + } + + private void OnModSettingChange( ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool _ ) + { + switch( type ) { - _collection = collection; - MetaManipulations = new MetaManager( _collection ); - _collection.ModSettingChanged += OnModSettingChange; - _collection.InheritanceChanged += OnInheritanceChange; - if( !Penumbra.CharacterUtility.Ready ) - { - Penumbra.CharacterUtility.LoadingFinished += IncrementCounter; - } - } - - public void Dispose() - { - MetaManipulations.Dispose(); - _collection.ModSettingChanged -= OnModSettingChange; - _collection.InheritanceChanged -= OnInheritanceChange; - Penumbra.CharacterUtility.LoadingFinished -= IncrementCounter; - } - - // Resolve a given game path according to this collection. - public FullPath? ResolvePath( Utf8GamePath gameResourcePath ) - { - if( !ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ) ) - { - return null; - } - - if( candidate.Path.InternalName.Length > Utf8GamePath.MaxGamePathLength - || candidate.Path.IsRooted && !candidate.Path.Exists ) - { - return null; - } - - return candidate.Path; - } - - // For a given full path, find all game paths that currently use this file. - public IEnumerable< Utf8GamePath > ReverseResolvePath( FullPath localFilePath ) - { - var needle = localFilePath.FullName.ToLower(); - if( localFilePath.IsRooted ) - { - needle = needle.Replace( '/', '\\' ); - } - - var iterator = ResolvedFiles - .Where( f => string.Equals( f.Value.Path.FullName, needle, StringComparison.OrdinalIgnoreCase ) ) - .Select( kvp => kvp.Key ); - - // For files that are not rooted, try to add themselves. - if( !localFilePath.IsRooted && Utf8GamePath.FromString( localFilePath.FullName, out var utf8 ) ) - { - iterator = iterator.Prepend( utf8 ); - } - - return iterator; - } - - // Reverse resolve multiple paths at once for efficiency. - public HashSet< Utf8GamePath >[] ReverseResolvePaths( IReadOnlyCollection< string > fullPaths ) - { - if( fullPaths.Count == 0 ) - return Array.Empty< HashSet< Utf8GamePath > >(); - - var ret = new HashSet< Utf8GamePath >[fullPaths.Count]; - var dict = new Dictionary< FullPath, int >( fullPaths.Count ); - foreach( var (path, idx) in fullPaths.WithIndex() ) - { - dict[ new FullPath(path) ] = idx; - ret[ idx ] = !Path.IsPathRooted( path ) && Utf8GamePath.FromString( path, out var utf8 ) - ? new HashSet< Utf8GamePath > { utf8 } - : new HashSet< Utf8GamePath >(); - } - - foreach( var (game, full) in ResolvedFiles ) - { - if( dict.TryGetValue( full.Path, out var idx ) ) + case ModSettingChange.Inheritance: + ReloadMod( Penumbra.ModManager[ modIdx ], true ); + break; + case ModSettingChange.EnableState: + if( oldValue == 0 ) { - ret[ idx ].Add( game ); + AddMod( Penumbra.ModManager[ modIdx ], true ); + } + else if( oldValue == 1 ) + { + RemoveMod( Penumbra.ModManager[ modIdx ], true ); + } + else if( _collection[ modIdx ].Settings?.Enabled == true ) + { + ReloadMod( Penumbra.ModManager[ modIdx ], true ); + } + else + { + RemoveMod( Penumbra.ModManager[ modIdx ], true ); + } + + break; + case ModSettingChange.Priority: + if( Conflicts( Penumbra.ModManager[ modIdx ] ).Count > 0 ) + { + ReloadMod( Penumbra.ModManager[ modIdx ], true ); + } + + break; + case ModSettingChange.Setting: + if( _collection[ modIdx ].Settings?.Enabled == true ) + { + ReloadMod( Penumbra.ModManager[ modIdx ], true ); + } + + break; + case ModSettingChange.MultiInheritance: + case ModSettingChange.MultiEnableState: + FullRecalculation(_collection == Penumbra.CollectionManager.Default); + break; + } + } + + // Inheritance changes are too big to check for relevance, + // just recompute everything. + private void OnInheritanceChange( bool _ ) + => FullRecalculation(_collection == Penumbra.CollectionManager.Default); + + public void FullRecalculation(bool isDefault) + { + ResolvedFiles.Clear(); + MetaManipulations.Reset(); + _conflicts.Clear(); + + // Add all forced redirects. + foreach( var tempMod in Penumbra.TempMods.ModsForAllCollections.Concat( + Penumbra.TempMods.Mods.TryGetValue( _collection, out var list ) ? list : Array.Empty< TemporaryMod >() ) ) + { + AddMod( tempMod, false ); + } + + foreach( var mod in Penumbra.ModManager ) + { + AddMod( mod, false ); + } + + AddMetaFiles(); + + ++_collection.ChangeCounter; + + if( isDefault && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) + { + Penumbra.ResidentResources.Reload(); + MetaManipulations.SetFiles(); + } + } + + public void ReloadMod( IMod mod, bool addMetaChanges ) + { + RemoveMod( mod, addMetaChanges ); + AddMod( mod, addMetaChanges ); + } + + public void RemoveMod( IMod mod, bool addMetaChanges ) + { + var conflicts = Conflicts( mod ); + + foreach( var (path, _) in mod.AllSubMods.SelectMany( s => s.Files.Concat( s.FileSwaps ) ) ) + { + if( !ResolvedFiles.TryGetValue( path, out var modPath ) ) + { + continue; + } + + if( modPath.Mod == mod ) + { + ResolvedFiles.Remove( path ); + } + } + + foreach( var manipulation in mod.AllSubMods.SelectMany( s => s.Manipulations ) ) + { + if( MetaManipulations.TryGetValue( manipulation, out var registeredMod ) && registeredMod == mod ) + { + MetaManipulations.RevertMod( manipulation ); + } + } + + _conflicts.Remove( mod ); + foreach( var conflict in conflicts ) + { + if( conflict.HasPriority ) + { + ReloadMod( conflict.Mod2, false ); + } + else + { + var newConflicts = Conflicts( conflict.Mod2 ).Remove( c => c.Mod2 == mod ); + if( newConflicts.Count > 0 ) + { + _conflicts[ conflict.Mod2 ] = newConflicts; + } + else + { + _conflicts.Remove( conflict.Mod2 ); } } - - return ret; } - private void OnModSettingChange( ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool _ ) + if( addMetaChanges ) { - switch( type ) - { - case ModSettingChange.Inheritance: - ReloadMod( Penumbra.ModManager[ modIdx ], true ); - break; - case ModSettingChange.EnableState: - if( oldValue == 0 ) - { - AddMod( Penumbra.ModManager[ modIdx ], true ); - } - else if( oldValue == 1 ) - { - RemoveMod( Penumbra.ModManager[ modIdx ], true ); - } - else if( _collection[ modIdx ].Settings?.Enabled == true ) - { - ReloadMod( Penumbra.ModManager[ modIdx ], true ); - } - else - { - RemoveMod( Penumbra.ModManager[ modIdx ], true ); - } - - break; - case ModSettingChange.Priority: - if( Conflicts( Penumbra.ModManager[ modIdx ] ).Count > 0 ) - { - ReloadMod( Penumbra.ModManager[ modIdx ], true ); - } - - break; - case ModSettingChange.Setting: - if( _collection[ modIdx ].Settings?.Enabled == true ) - { - ReloadMod( Penumbra.ModManager[ modIdx ], true ); - } - - break; - case ModSettingChange.MultiInheritance: - case ModSettingChange.MultiEnableState: - FullRecalculation(_collection == Penumbra.CollectionManager.Default); - break; - } - } - - // Inheritance changes are too big to check for relevance, - // just recompute everything. - private void OnInheritanceChange( bool _ ) - => FullRecalculation(_collection == Penumbra.CollectionManager.Default); - - public void FullRecalculation(bool isDefault) - { - ResolvedFiles.Clear(); - MetaManipulations.Reset(); - _conflicts.Clear(); - - // Add all forced redirects. - foreach( var tempMod in Penumbra.TempMods.ModsForAllCollections.Concat( - Penumbra.TempMods.Mods.TryGetValue( _collection, out var list ) ? list : Array.Empty< TemporaryMod >() ) ) - { - AddMod( tempMod, false ); - } - - foreach( var mod in Penumbra.ModManager ) - { - AddMod( mod, false ); - } - - AddMetaFiles(); - ++_collection.ChangeCounter; - - if( isDefault && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) + if( _collection == Penumbra.CollectionManager.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) { Penumbra.ResidentResources.Reload(); MetaManipulations.SetFiles(); } } + } - public void ReloadMod( IMod mod, bool addMetaChanges ) + + // Add all files and possibly manipulations of a given mod according to its settings in this collection. + public void AddMod( IMod mod, bool addMetaChanges ) + { + if( mod.Index >= 0 ) { - RemoveMod( mod, addMetaChanges ); - AddMod( mod, addMetaChanges ); - } - - public void RemoveMod( IMod mod, bool addMetaChanges ) - { - var conflicts = Conflicts( mod ); - - foreach( var (path, _) in mod.AllSubMods.SelectMany( s => s.Files.Concat( s.FileSwaps ) ) ) + var settings = _collection[ mod.Index ].Settings; + if( settings is not { Enabled: true } ) { - if( !ResolvedFiles.TryGetValue( path, out var modPath ) ) + return; + } + + foreach( var (group, groupIndex) in mod.Groups.WithIndex().OrderByDescending( g => g.Item1.Priority ) ) + { + if( group.Count == 0 ) { continue; } - if( modPath.Mod == mod ) + var config = settings.Settings[ groupIndex ]; + switch( group.Type ) { - ResolvedFiles.Remove( path ); - } - } - - foreach( var manipulation in mod.AllSubMods.SelectMany( s => s.Manipulations ) ) - { - if( MetaManipulations.TryGetValue( manipulation, out var registeredMod ) && registeredMod == mod ) - { - MetaManipulations.RevertMod( manipulation ); - } - } - - _conflicts.Remove( mod ); - foreach( var conflict in conflicts ) - { - if( conflict.HasPriority ) - { - ReloadMod( conflict.Mod2, false ); - } - else - { - var newConflicts = Conflicts( conflict.Mod2 ).Remove( c => c.Mod2 == mod ); - if( newConflicts.Count > 0 ) + case GroupType.Single: + AddSubMod( group[ ( int )config ], mod ); + break; + case GroupType.Multi: { - _conflicts[ conflict.Mod2 ] = newConflicts; - } - else - { - _conflicts.Remove( conflict.Mod2 ); - } - } - } - - if( addMetaChanges ) - { - ++_collection.ChangeCounter; - if( _collection == Penumbra.CollectionManager.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) - { - Penumbra.ResidentResources.Reload(); - MetaManipulations.SetFiles(); - } - } - } - - - // Add all files and possibly manipulations of a given mod according to its settings in this collection. - public void AddMod( IMod mod, bool addMetaChanges ) - { - if( mod.Index >= 0 ) - { - var settings = _collection[ mod.Index ].Settings; - if( settings is not { Enabled: true } ) - { - return; - } - - foreach( var (group, groupIndex) in mod.Groups.WithIndex().OrderByDescending( g => g.Item1.Priority ) ) - { - if( group.Count == 0 ) - { - continue; - } - - var config = settings.Settings[ groupIndex ]; - switch( group.Type ) - { - case GroupType.Single: - AddSubMod( group[ ( int )config ], mod ); - break; - case GroupType.Multi: + foreach( var (option, _) in group.WithIndex() + .Where( p => ( ( 1 << p.Item2 ) & config ) != 0 ) + .OrderByDescending( p => group.OptionPriority( p.Item2 ) ) ) { - foreach( var (option, _) in group.WithIndex() - .Where( p => ( ( 1 << p.Item2 ) & config ) != 0 ) - .OrderByDescending( p => group.OptionPriority( p.Item2 ) ) ) - { - AddSubMod( option, mod ); - } - - break; + AddSubMod( option, mod ); } + + break; } } } - - AddSubMod( mod.Default, mod ); - - if( addMetaChanges ) - { - ++_collection.ChangeCounter; - if( mod.TotalManipulations > 0 ) - { - AddMetaFiles(); - } - - if( _collection == Penumbra.CollectionManager.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) - { - Penumbra.ResidentResources.Reload(); - MetaManipulations.SetFiles(); - } - } } - // Add all files and possibly manipulations of a specific submod - private void AddSubMod( ISubMod subMod, IMod parentMod ) - { - foreach( var (path, file) in subMod.Files.Concat( subMod.FileSwaps ) ) - { - AddFile( path, file, parentMod ); - } + AddSubMod( mod.Default, mod ); - foreach( var manip in subMod.Manipulations ) - { - AddManipulation( manip, parentMod ); - } - } - - // Add a specific file redirection, handling potential conflicts. - // For different mods, higher mod priority takes precedence before option group priority, - // which takes precedence before option priority, which takes precedence before ordering. - // Inside the same mod, conflicts are not recorded. - private void AddFile( Utf8GamePath path, FullPath file, IMod mod ) - { - if( !CheckFullPath( path, file ) ) - { - return; - } - - if( ResolvedFiles.TryAdd( path, new ModPath( mod, file ) ) ) - { - return; - } - - var modPath = ResolvedFiles[ path ]; - // Lower prioritized option in the same mod. - if( mod == modPath.Mod ) - { - return; - } - - if( AddConflict( path, mod, modPath.Mod ) ) - { - ResolvedFiles[ path ] = new ModPath( mod, file ); - } - } - - - // Remove all empty conflict sets for a given mod with the given conflicts. - // If transitive is true, also removes the corresponding version of the other mod. - private void RemoveEmptyConflicts( IMod mod, SingleArray< ModConflicts > oldConflicts, bool transitive ) - { - var changedConflicts = oldConflicts.Remove( c => - { - if( c.Conflicts.Count == 0 ) - { - if( transitive ) - { - RemoveEmptyConflicts( c.Mod2, Conflicts( c.Mod2 ), false ); - } - - return true; - } - - return false; - } ); - if( changedConflicts.Count == 0 ) - { - _conflicts.Remove( mod ); - } - else - { - _conflicts[ mod ] = changedConflicts; - } - } - - // Add a new conflict between the added mod and the existing mod. - // Update all other existing conflicts between the existing mod and other mods if necessary. - // Returns if the added mod takes priority before the existing mod. - private bool AddConflict( object data, IMod addedMod, IMod existingMod ) - { - var addedPriority = addedMod.Index >= 0 ? _collection[ addedMod.Index ].Settings!.Priority : addedMod.Priority; - var existingPriority = existingMod.Index >= 0 ? _collection[ existingMod.Index ].Settings!.Priority : existingMod.Priority; - - if( existingPriority < addedPriority ) - { - var tmpConflicts = Conflicts( existingMod ); - foreach( var conflict in tmpConflicts ) - { - if( data is Utf8GamePath path && conflict.Conflicts.RemoveAll( p => p is Utf8GamePath x && x.Equals( path ) ) > 0 - || data is MetaManipulation meta && conflict.Conflicts.RemoveAll( m => m is MetaManipulation x && x.Equals( meta ) ) > 0 ) - { - AddConflict( data, addedMod, conflict.Mod2 ); - } - } - - RemoveEmptyConflicts( existingMod, tmpConflicts, true ); - } - - var addedConflicts = Conflicts( addedMod ); - var existingConflicts = Conflicts( existingMod ); - if( addedConflicts.FindFirst( c => c.Mod2 == existingMod, out var oldConflicts ) ) - { - // Only need to change one list since both conflict lists refer to the same list. - oldConflicts.Conflicts.Add( data ); - } - else - { - // Add the same conflict list to both conflict directions. - var conflictList = new List< object > { data }; - _conflicts[ addedMod ] = addedConflicts.Append( new ModConflicts( existingMod, conflictList, existingPriority < addedPriority, - existingPriority != addedPriority ) ); - _conflicts[ existingMod ] = existingConflicts.Append( new ModConflicts( addedMod, conflictList, - existingPriority >= addedPriority, - existingPriority != addedPriority ) ); - } - - return existingPriority < addedPriority; - } - - // Add a specific manipulation, handling potential conflicts. - // For different mods, higher mod priority takes precedence before option group priority, - // which takes precedence before option priority, which takes precedence before ordering. - // Inside the same mod, conflicts are not recorded. - private void AddManipulation( MetaManipulation manip, IMod mod ) - { - if( !MetaManipulations.TryGetValue( manip, out var existingMod ) ) - { - MetaManipulations.ApplyMod( manip, mod ); - return; - } - - // Lower prioritized option in the same mod. - if( mod == existingMod ) - { - return; - } - - if( AddConflict( manip, mod, existingMod ) ) - { - MetaManipulations.ApplyMod( manip, mod ); - } - } - - - // Add all necessary meta file redirects. - private void AddMetaFiles() - => MetaManipulations.SetImcFiles(); - - // Increment the counter to ensure new files are loaded after applying meta changes. - private void IncrementCounter() + if( addMetaChanges ) { ++_collection.ChangeCounter; - Penumbra.CharacterUtility.LoadingFinished -= IncrementCounter; - } - - - // Identify and record all manipulated objects for this entire collection. - private void SetChangedItems() - { - if( _changedItemsSaveCounter == _collection.ChangeCounter ) + if( mod.TotalManipulations > 0 ) { - return; + AddMetaFiles(); } - try + if( _collection == Penumbra.CollectionManager.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) { - _changedItemsSaveCounter = _collection.ChangeCounter; - _changedItems.Clear(); - // Skip IMCs because they would result in far too many false-positive items, - // since they are per set instead of per item-slot/item/variant. - var identifier = Penumbra.Identifier; - var items = new SortedList< string, object? >( 512 ); - - void AddItems( IMod mod ) - { - foreach( var (name, obj) in items ) - { - if( !_changedItems.TryGetValue( name, out var data ) ) - { - _changedItems.Add( name, ( new SingleArray< IMod >( mod ), obj ) ); - } - else if( !data.Item1.Contains( mod ) ) - { - _changedItems[ name ] = ( data.Item1.Append( mod ), obj is int x && data.Item2 is int y ? x + y : obj ); - } - else if( obj is int x && data.Item2 is int y ) - { - _changedItems[ name ] = ( data.Item1, x + y ); - } - } - - items.Clear(); - } - - foreach( var (resolved, modPath) in ResolvedFiles.Where( file => !file.Key.Path.EndsWith( "imc"u8 ) ) ) - { - identifier.Identify( items, resolved.ToString() ); - AddItems( modPath.Mod ); - } - - foreach( var (manip, mod) in MetaManipulations ) - { - Mod.ComputeChangedItems( items, manip ); - AddItems( mod ); - } - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Unknown Error:\n{e}" ); + Penumbra.ResidentResources.Reload(); + MetaManipulations.SetFiles(); } } } + + // Add all files and possibly manipulations of a specific submod + private void AddSubMod( ISubMod subMod, IMod parentMod ) + { + foreach( var (path, file) in subMod.Files.Concat( subMod.FileSwaps ) ) + { + AddFile( path, file, parentMod ); + } + + foreach( var manip in subMod.Manipulations ) + { + AddManipulation( manip, parentMod ); + } + } + + // Add a specific file redirection, handling potential conflicts. + // For different mods, higher mod priority takes precedence before option group priority, + // which takes precedence before option priority, which takes precedence before ordering. + // Inside the same mod, conflicts are not recorded. + private void AddFile( Utf8GamePath path, FullPath file, IMod mod ) + { + if( !ModCollection.CheckFullPath( path, file ) ) + { + return; + } + + if( ResolvedFiles.TryAdd( path, new ModPath( mod, file ) ) ) + { + return; + } + + var modPath = ResolvedFiles[ path ]; + // Lower prioritized option in the same mod. + if( mod == modPath.Mod ) + { + return; + } + + if( AddConflict( path, mod, modPath.Mod ) ) + { + ResolvedFiles[ path ] = new ModPath( mod, file ); + } + } + + + // Remove all empty conflict sets for a given mod with the given conflicts. + // If transitive is true, also removes the corresponding version of the other mod. + private void RemoveEmptyConflicts( IMod mod, SingleArray< ModConflicts > oldConflicts, bool transitive ) + { + var changedConflicts = oldConflicts.Remove( c => + { + if( c.Conflicts.Count == 0 ) + { + if( transitive ) + { + RemoveEmptyConflicts( c.Mod2, Conflicts( c.Mod2 ), false ); + } + + return true; + } + + return false; + } ); + if( changedConflicts.Count == 0 ) + { + _conflicts.Remove( mod ); + } + else + { + _conflicts[ mod ] = changedConflicts; + } + } + + // Add a new conflict between the added mod and the existing mod. + // Update all other existing conflicts between the existing mod and other mods if necessary. + // Returns if the added mod takes priority before the existing mod. + private bool AddConflict( object data, IMod addedMod, IMod existingMod ) + { + var addedPriority = addedMod.Index >= 0 ? _collection[ addedMod.Index ].Settings!.Priority : addedMod.Priority; + var existingPriority = existingMod.Index >= 0 ? _collection[ existingMod.Index ].Settings!.Priority : existingMod.Priority; + + if( existingPriority < addedPriority ) + { + var tmpConflicts = Conflicts( existingMod ); + foreach( var conflict in tmpConflicts ) + { + if( data is Utf8GamePath path && conflict.Conflicts.RemoveAll( p => p is Utf8GamePath x && x.Equals( path ) ) > 0 + || data is MetaManipulation meta && conflict.Conflicts.RemoveAll( m => m is MetaManipulation x && x.Equals( meta ) ) > 0 ) + { + AddConflict( data, addedMod, conflict.Mod2 ); + } + } + + RemoveEmptyConflicts( existingMod, tmpConflicts, true ); + } + + var addedConflicts = Conflicts( addedMod ); + var existingConflicts = Conflicts( existingMod ); + if( addedConflicts.FindFirst( c => c.Mod2 == existingMod, out var oldConflicts ) ) + { + // Only need to change one list since both conflict lists refer to the same list. + oldConflicts.Conflicts.Add( data ); + } + else + { + // Add the same conflict list to both conflict directions. + var conflictList = new List< object > { data }; + _conflicts[ addedMod ] = addedConflicts.Append( new ModConflicts( existingMod, conflictList, existingPriority < addedPriority, + existingPriority != addedPriority ) ); + _conflicts[ existingMod ] = existingConflicts.Append( new ModConflicts( addedMod, conflictList, + existingPriority >= addedPriority, + existingPriority != addedPriority ) ); + } + + return existingPriority < addedPriority; + } + + // Add a specific manipulation, handling potential conflicts. + // For different mods, higher mod priority takes precedence before option group priority, + // which takes precedence before option priority, which takes precedence before ordering. + // Inside the same mod, conflicts are not recorded. + private void AddManipulation( MetaManipulation manip, IMod mod ) + { + if( !MetaManipulations.TryGetValue( manip, out var existingMod ) ) + { + MetaManipulations.ApplyMod( manip, mod ); + return; + } + + // Lower prioritized option in the same mod. + if( mod == existingMod ) + { + return; + } + + if( AddConflict( manip, mod, existingMod ) ) + { + MetaManipulations.ApplyMod( manip, mod ); + } + } + + + // Add all necessary meta file redirects. + private void AddMetaFiles() + => MetaManipulations.SetImcFiles(); + + // Increment the counter to ensure new files are loaded after applying meta changes. + private void IncrementCounter() + { + ++_collection.ChangeCounter; + Penumbra.CharacterUtility.LoadingFinished -= IncrementCounter; + } + + + // Identify and record all manipulated objects for this entire collection. + private void SetChangedItems() + { + if( _changedItemsSaveCounter == _collection.ChangeCounter ) + { + return; + } + + try + { + _changedItemsSaveCounter = _collection.ChangeCounter; + _changedItems.Clear(); + // Skip IMCs because they would result in far too many false-positive items, + // since they are per set instead of per item-slot/item/variant. + var identifier = Penumbra.Identifier; + var items = new SortedList< string, object? >( 512 ); + + void AddItems( IMod mod ) + { + foreach( var (name, obj) in items ) + { + if( !_changedItems.TryGetValue( name, out var data ) ) + { + _changedItems.Add( name, ( new SingleArray< IMod >( mod ), obj ) ); + } + else if( !data.Item1.Contains( mod ) ) + { + _changedItems[ name ] = ( data.Item1.Append( mod ), obj is int x && data.Item2 is int y ? x + y : obj ); + } + else if( obj is int x && data.Item2 is int y ) + { + _changedItems[ name ] = ( data.Item1, x + y ); + } + } + + items.Clear(); + } + + foreach( var (resolved, modPath) in ResolvedFiles.Where( file => !file.Key.Path.EndsWith( "imc"u8 ) ) ) + { + identifier.Identify( items, resolved.ToString() ); + AddItems( modPath.Mod ); + } + + foreach( var (manip, mod) in MetaManipulations ) + { + Mod.ComputeChangedItems( items, manip ); + AddItems( mod ); + } + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Unknown Error:\n{e}" ); + } + } } \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.File.cs b/Penumbra/Collections/ModCollection.File.cs index c2516519..dd632f71 100644 --- a/Penumbra/Collections/ModCollection.File.cs +++ b/Penumbra/Collections/ModCollection.File.cs @@ -15,7 +15,7 @@ public partial class ModCollection : ISavable { // Since inheritances depend on other collections existing, // we return them as a list to be applied after reading all collections. - private static ModCollection? LoadFromFile(FileInfo file, out IReadOnlyList inheritance) + internal static ModCollection? LoadFromFile(FileInfo file, out IReadOnlyList inheritance) { inheritance = Array.Empty(); if (!file.Exists) diff --git a/Penumbra/Collections/ModCollection.Inheritance.cs b/Penumbra/Collections/ModCollection.Inheritance.cs index d9eecdbd..4c694f88 100644 --- a/Penumbra/Collections/ModCollection.Inheritance.cs +++ b/Penumbra/Collections/ModCollection.Inheritance.cs @@ -16,7 +16,7 @@ public partial class ModCollection // The bool signifies whether the change was in an already inherited collection. public event Action< bool > InheritanceChanged; - private readonly List< ModCollection > _inheritance = new(); + internal readonly List< ModCollection > _inheritance = new(); public IReadOnlyList< ModCollection > Inheritance => _inheritance; @@ -98,7 +98,7 @@ public partial class ModCollection Penumbra.Log.Debug( $"Removed {inheritance.AnonymizedName} from {AnonymizedName} inheritances." ); } - private void ClearSubscriptions( ModCollection other ) + internal void ClearSubscriptions( ModCollection other ) { other.ModSettingChanged -= OnInheritedModSettingChange; other.InheritanceChanged -= OnInheritedInheritanceChange; diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 54f1f9a8..6215dc03 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -23,18 +23,18 @@ public partial class ModCollection // The collection name can contain invalid path characters, // but after removing those and going to lower case it has to be unique. - public string Name { get; private init; } + public string Name { get; internal init; } // Get the first two letters of a collection name and its Index (or None if it is the empty collection). public string AnonymizedName => this == Empty ? Empty.Name : Name.Length > 2 ? $"{Name[..2]}... ({Index})" : $"{Name} ({Index})"; - public int Version { get; private set; } - public int Index { get; private set; } = -1; + public int Version { get; internal set; } + public int Index { get; internal set; } = -1; // If a ModSetting is null, it can be inherited from other collections. // If no collection provides a setting for the mod, it is just disabled. - private readonly List _settings; + internal readonly List _settings; public IReadOnlyList Settings => _settings; @@ -115,7 +115,7 @@ public partial class ModCollection } // Add settings for a new appended mod, by checking if the mod had settings from a previous deletion. - private bool AddMod(Mod mod) + internal bool AddMod(Mod mod) { if (_unusedSettings.TryGetValue(mod.ModPath.Name, out var save)) { @@ -130,7 +130,7 @@ public partial class ModCollection } // Move settings from the current mod list to the unused mod settings. - private void RemoveMod(Mod mod, int idx) + internal void RemoveMod(Mod mod, int idx) { var settings = _settings[idx]; if (settings != null) @@ -150,7 +150,7 @@ public partial class ModCollection } // Move all settings to unused settings for rediscovery. - private void PrepareModDiscovery() + internal void PrepareModDiscovery() { foreach (var (mod, setting) in Penumbra.ModManager.Zip(_settings).Where(s => s.Second != null)) _unusedSettings[mod.ModPath.Name] = new ModSettings.SavedSettings(setting!, mod); @@ -160,7 +160,7 @@ public partial class ModCollection // Apply all mod settings from unused settings to the current set of mods. // Also fixes invalid settings. - private void ApplyModSettings() + internal void ApplyModSettings() { _settings.Capacity = Math.Max(_settings.Capacity, Penumbra.ModManager.Count); if (Penumbra.ModManager.Aggregate(false, (current, mod) => current | AddMod(mod))) diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 97abbbef..bb1ce79a 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -27,12 +27,12 @@ public class CommandHandler : IDisposable private readonly Configuration _config; private readonly ConfigWindow _configWindow; private readonly ActorManager _actors; - private readonly Mod.Manager _modManager; - private readonly ModCollection.Manager _collectionManager; + private readonly ModManager _modManager; + private readonly CollectionManager _collectionManager; private readonly Penumbra _penumbra; public CommandHandler(Framework framework, CommandManager commandManager, ChatGui chat, RedrawService redrawService, Configuration config, - ConfigWindow configWindow, Mod.Manager modManager, ModCollection.Manager collectionManager, ActorService actors, Penumbra penumbra) + ConfigWindow configWindow, ModManager modManager, CollectionManager collectionManager, ActorService actors, Penumbra penumbra) { _commandManager = commandManager; _redrawService = redrawService; diff --git a/Penumbra/Import/TexToolsImport.cs b/Penumbra/Import/TexToolsImport.cs index 974c8b2e..56b39f6e 100644 --- a/Penumbra/Import/TexToolsImport.cs +++ b/Penumbra/Import/TexToolsImport.cs @@ -36,10 +36,10 @@ public partial class TexToolsImporter : IDisposable private readonly Configuration _config; private readonly ModEditor _editor; - private readonly Mod.Manager _modManager; + private readonly ModManager _modManager; public TexToolsImporter( DirectoryInfo baseDirectory, int count, IEnumerable< FileInfo > modPackFiles, - Action< FileInfo, DirectoryInfo?, Exception? > handler, Configuration config, ModEditor editor, Mod.Manager modManager) + Action< FileInfo, DirectoryInfo?, Exception? > handler, Configuration config, ModEditor editor, ModManager modManager) { _baseDirectory = baseDirectory; _tmpFile = Path.Combine( _baseDirectory.FullName, TempFileName ); diff --git a/Penumbra/Interop/PathResolving/CollectionResolver.cs b/Penumbra/Interop/PathResolving/CollectionResolver.cs index e4a7a0a9..cb271d7e 100644 --- a/Penumbra/Interop/PathResolving/CollectionResolver.cs +++ b/Penumbra/Interop/PathResolving/CollectionResolver.cs @@ -31,12 +31,12 @@ public unsafe class CollectionResolver private readonly CutsceneService _cutscenes; private readonly Configuration _config; - private readonly ModCollection.Manager _collectionManager; + private readonly CollectionManager _collectionManager; private readonly TempCollectionManager _tempCollections; private readonly DrawObjectState _drawObjectState; public CollectionResolver(PerformanceTracker performance, IdentifiedCollectionCache cache, ClientState clientState, GameGui gameGui, - DataManager gameData, ActorService actors, CutsceneService cutscenes, Configuration config, ModCollection.Manager collectionManager, + DataManager gameData, ActorService actors, CutsceneService cutscenes, Configuration config, CollectionManager collectionManager, TempCollectionManager tempCollections, DrawObjectState drawObjectState) { _performance = performance; diff --git a/Penumbra/Interop/PathResolving/PathResolver.cs b/Penumbra/Interop/PathResolving/PathResolver.cs index ea1bb2b1..734971c5 100644 --- a/Penumbra/Interop/PathResolving/PathResolver.cs +++ b/Penumbra/Interop/PathResolving/PathResolver.cs @@ -16,7 +16,7 @@ public class PathResolver : IDisposable { private readonly PerformanceTracker _performance; private readonly Configuration _config; - private readonly ModCollection.Manager _collectionManager; + private readonly CollectionManager _collectionManager; private readonly TempCollectionManager _tempCollections; private readonly ResourceLoader _loader; @@ -25,7 +25,7 @@ public class PathResolver : IDisposable private readonly PathState _pathState; private readonly MetaState _metaState; - public unsafe PathResolver(PerformanceTracker performance, Configuration config, ModCollection.Manager collectionManager, + public unsafe PathResolver(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, TempCollectionManager tempCollections, ResourceLoader loader, AnimationHookService animationHookService, SubfileHelper subfileHelper, PathState pathState, MetaState metaState) { diff --git a/Penumbra/Mods/Editor/DuplicateManager.cs b/Penumbra/Mods/Editor/DuplicateManager.cs index 8f480851..2d3026ba 100644 --- a/Penumbra/Mods/Editor/DuplicateManager.cs +++ b/Penumbra/Mods/Editor/DuplicateManager.cs @@ -11,12 +11,12 @@ namespace Penumbra.Mods; public class DuplicateManager { - private readonly Mod.Manager _modManager; + private readonly ModManager _modManager; private readonly SHA256 _hasher = SHA256.Create(); private readonly ModFileCollection _files; private readonly List<(FullPath[] Paths, long Size, byte[] Hash)> _duplicates = new(); - public DuplicateManager(ModFileCollection files, Mod.Manager modManager) + public DuplicateManager(ModFileCollection files, ModManager modManager) { _files = files; _modManager = modManager; @@ -80,7 +80,7 @@ public class DuplicateManager } else { - var sub = (Mod.SubMod)subMod; + var sub = (SubMod)subMod; sub.FileData = dict; if (groupIdx == -1) mod.SaveDefaultMod(); diff --git a/Penumbra/Mods/Editor/ModBackup.cs b/Penumbra/Mods/Editor/ModBackup.cs index 21f8792b..e2ab4994 100644 --- a/Penumbra/Mods/Editor/ModBackup.cs +++ b/Penumbra/Mods/Editor/ModBackup.cs @@ -10,12 +10,12 @@ public class ModBackup { public static bool CreatingBackup { get; private set; } - private readonly Mod.Manager _modManager; + private readonly ModManager _modManager; private readonly Mod _mod; public readonly string Name; public readonly bool Exists; - public ModBackup(Mod.Manager modManager, Mod mod) + public ModBackup(ModManager modManager, Mod mod) { _modManager = modManager; _mod = mod; @@ -24,9 +24,9 @@ public class ModBackup } /// Migrate file extensions. - public static void MigrateZipToPmp(Mod.Manager manager) + public static void MigrateZipToPmp(ModManager modManager) { - foreach (var mod in manager) + foreach (var mod in modManager) { var pmpName = mod.ModPath + ".pmp"; var zipName = mod.ModPath + ".zip"; diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index 5215973c..29a06c44 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -9,11 +9,11 @@ namespace Penumbra.Mods; public class ModFileEditor { private readonly ModFileCollection _files; - private readonly Mod.Manager _modManager; + private readonly ModManager _modManager; public bool Changes { get; private set; } - public ModFileEditor(ModFileCollection files, Mod.Manager modManager) + public ModFileEditor(ModFileCollection files, ModManager modManager) { _files = files; _modManager = modManager; @@ -24,7 +24,7 @@ public class ModFileEditor Changes = false; } - public int Apply(Mod mod, Mod.SubMod option) + public int Apply(Mod mod, SubMod option) { var dict = new Dictionary(); var num = 0; diff --git a/Penumbra/Mods/Editor/ModMetaEditor.cs b/Penumbra/Mods/Editor/ModMetaEditor.cs index a211398b..f536935d 100644 --- a/Penumbra/Mods/Editor/ModMetaEditor.cs +++ b/Penumbra/Mods/Editor/ModMetaEditor.cs @@ -6,7 +6,7 @@ namespace Penumbra.Mods; public class ModMetaEditor { - private readonly Mod.Manager _modManager; + private readonly ModManager _modManager; private readonly HashSet _imc = new(); private readonly HashSet _eqp = new(); @@ -15,7 +15,7 @@ public class ModMetaEditor private readonly HashSet _est = new(); private readonly HashSet _rsp = new(); - public ModMetaEditor(Mod.Manager modManager) + public ModMetaEditor(ModManager modManager) => _modManager = modManager; public bool Changes { get; private set; } = false; diff --git a/Penumbra/Mods/Editor/ModNormalizer.cs b/Penumbra/Mods/Editor/ModNormalizer.cs index 9fc02d77..aff491c7 100644 --- a/Penumbra/Mods/Editor/ModNormalizer.cs +++ b/Penumbra/Mods/Editor/ModNormalizer.cs @@ -11,7 +11,7 @@ namespace Penumbra.Mods; public class ModNormalizer { - private readonly Mod.Manager _modManager; + private readonly ModManager _modManager; private readonly List>> _redirections = new(); public Mod Mod { get; private set; } = null!; @@ -24,7 +24,7 @@ public class ModNormalizer public bool Running => Step < TotalSteps; - public ModNormalizer(Mod.Manager modManager) + public ModNormalizer(ModManager modManager) => _modManager = modManager; public void Normalize(Mod mod) @@ -177,7 +177,7 @@ public class ModNormalizer _redirections[groupIdx + 1].Add(new Dictionary()); var groupDir = Mod.Creator.CreateModFolder(directory, group.Name); - foreach (var option in group.OfType()) + foreach (var option in group.OfType()) { var optionDir = Mod.Creator.CreateModFolder(groupDir, option.Name); @@ -279,7 +279,7 @@ public class ModNormalizer private void ApplyRedirections() { - foreach (var option in Mod.AllSubMods.OfType()) + foreach (var option in Mod.AllSubMods.OfType()) { _modManager.OptionSetFiles(Mod, option.GroupIdx, option.OptionIdx, _redirections[option.GroupIdx + 1][option.OptionIdx]); } diff --git a/Penumbra/Mods/Editor/ModSwapEditor.cs b/Penumbra/Mods/Editor/ModSwapEditor.cs index 0237d08f..29da93c1 100644 --- a/Penumbra/Mods/Editor/ModSwapEditor.cs +++ b/Penumbra/Mods/Editor/ModSwapEditor.cs @@ -5,13 +5,13 @@ using Penumbra.Util; public class ModSwapEditor { - private readonly Mod.Manager _modManager; + private readonly ModManager _modManager; private readonly Dictionary _swaps = new(); public IReadOnlyDictionary Swaps => _swaps; - public ModSwapEditor(Mod.Manager modManager) + public ModSwapEditor(ModManager modManager) => _modManager = modManager; public void Revert(ISubMod option) diff --git a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs index 65e3e10f..96e206fa 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs @@ -4,202 +4,199 @@ using System.Linq; namespace Penumbra.Mods; -public partial class Mod +public partial class ModManager { - public partial class Manager + public delegate void ModPathChangeDelegate(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, + DirectoryInfo? newDirectory); + + public event ModPathChangeDelegate ModPathChanged; + + // Rename/Move a mod directory. + // Updates all collection settings and sort order settings. + public void MoveModDirectory(int idx, string newName) { - public delegate void ModPathChangeDelegate(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, - DirectoryInfo? newDirectory); + var mod = this[idx]; + var oldName = mod.Name; + var oldDirectory = mod.ModPath; - public event ModPathChangeDelegate ModPathChanged; - - // Rename/Move a mod directory. - // Updates all collection settings and sort order settings. - public void MoveModDirectory(int idx, string newName) + switch (NewDirectoryValid(oldDirectory.Name, newName, out var dir)) { - var mod = this[idx]; - var oldName = mod.Name; - var oldDirectory = mod.ModPath; - - switch (NewDirectoryValid(oldDirectory.Name, newName, out var dir)) - { - case NewDirectoryState.NonExisting: - // Nothing to do - break; - case NewDirectoryState.ExistsEmpty: - try - { - Directory.Delete(dir!.FullName); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not delete empty directory {dir!.FullName} to move {mod.Name} to it:\n{e}"); - return; - } - - break; - // Should be caught beforehand. - case NewDirectoryState.ExistsNonEmpty: - case NewDirectoryState.ExistsAsFile: - case NewDirectoryState.ContainsInvalidSymbols: - // Nothing to do at all. - case NewDirectoryState.Identical: - default: - return; - } - - try - { - Directory.Move(oldDirectory.FullName, dir!.FullName); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not move {mod.Name} from {oldDirectory.Name} to {dir!.Name}:\n{e}"); - return; - } - - DataEditor.MoveDataFile(oldDirectory, dir); - new ModBackup(this, mod).Move(null, dir.Name); - - dir.Refresh(); - mod.ModPath = dir; - if (!mod.Reload(this, false, out var metaChange)) - { - Penumbra.Log.Error($"Error reloading moved mod {mod.Name}."); - return; - } - - ModPathChanged.Invoke(ModPathChangeType.Moved, mod, oldDirectory, dir); - if (metaChange != ModDataChangeType.None) - _communicator.ModDataChanged.Invoke(metaChange, mod, oldName); - } - - /// - /// Reload a mod without changing its base directory. - /// If the base directory does not exist anymore, the mod will be deleted. - /// - public void ReloadMod(int idx) - { - var mod = this[idx]; - var oldName = mod.Name; - - ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath); - if (!mod.Reload(this, true, out var metaChange)) - { - Penumbra.Log.Warning(mod.Name.Length == 0 - ? $"Reloading mod {oldName} has failed, new name is empty. Deleting instead." - : $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it ha. Deleting instead."); - - DeleteMod(idx); - return; - } - - ModPathChanged.Invoke(ModPathChangeType.Reloaded, mod, mod.ModPath, mod.ModPath); - if (metaChange != ModDataChangeType.None) - _communicator.ModDataChanged.Invoke(metaChange, mod, oldName); - } - - /// - /// Delete a mod by its index. The event is invoked before the mod is removed from the list. - /// Deletes from filesystem as well as from internal data. - /// Updates indices of later mods. - /// - public void DeleteMod(int idx) - { - var mod = this[idx]; - if (Directory.Exists(mod.ModPath.FullName)) + case NewDirectoryState.NonExisting: + // Nothing to do + break; + case NewDirectoryState.ExistsEmpty: try { - Directory.Delete(mod.ModPath.FullName, true); - Penumbra.Log.Debug($"Deleted directory {mod.ModPath.FullName} for {mod.Name}."); + Directory.Delete(dir!.FullName); } catch (Exception e) { - Penumbra.Log.Error($"Could not delete the mod {mod.ModPath.Name}:\n{e}"); + Penumbra.Log.Error($"Could not delete empty directory {dir!.FullName} to move {mod.Name} to it:\n{e}"); + return; } - ModPathChanged.Invoke(ModPathChangeType.Deleted, mod, mod.ModPath, null); - _mods.RemoveAt(idx); - foreach (var remainingMod in _mods.Skip(idx)) - --remainingMod.Index; - - Penumbra.Log.Debug($"Deleted mod {mod.Name}."); - } - - /// Load a new mod and add it to the manager if successful. - public void AddMod(DirectoryInfo modFolder) - { - if (_mods.Any(m => m.ModPath.Name == modFolder.Name)) + break; + // Should be caught beforehand. + case NewDirectoryState.ExistsNonEmpty: + case NewDirectoryState.ExistsAsFile: + case NewDirectoryState.ContainsInvalidSymbols: + // Nothing to do at all. + case NewDirectoryState.Identical: + default: return; - - Creator.SplitMultiGroups(modFolder); - var mod = LoadMod(this, modFolder, true); - if (mod == null) - return; - - mod.Index = _mods.Count; - _mods.Add(mod); - ModPathChanged.Invoke(ModPathChangeType.Added, mod, null, mod.ModPath); - Penumbra.Log.Debug($"Added new mod {mod.Name} from {modFolder.FullName}."); } - public enum NewDirectoryState + try { - NonExisting, - ExistsEmpty, - ExistsNonEmpty, - ExistsAsFile, - ContainsInvalidSymbols, - Identical, - Empty, + Directory.Move(oldDirectory.FullName, dir!.FullName); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not move {mod.Name} from {oldDirectory.Name} to {dir!.Name}:\n{e}"); + return; } - /// Return the state of the new potential name of a directory. - public NewDirectoryState NewDirectoryValid(string oldName, string newName, out DirectoryInfo? directory) + DataEditor.MoveDataFile(oldDirectory, dir); + new ModBackup(this, mod).Move(null, dir.Name); + + dir.Refresh(); + mod.ModPath = dir; + if (!mod.Reload(this, false, out var metaChange)) { - directory = null; - if (newName.Length == 0) - return NewDirectoryState.Empty; - - if (oldName == newName) - return NewDirectoryState.Identical; - - var fixedNewName = Creator.ReplaceBadXivSymbols(newName); - if (fixedNewName != newName) - return NewDirectoryState.ContainsInvalidSymbols; - - directory = new DirectoryInfo(Path.Combine(BasePath.FullName, fixedNewName)); - if (File.Exists(directory.FullName)) - return NewDirectoryState.ExistsAsFile; - - if (!Directory.Exists(directory.FullName)) - return NewDirectoryState.NonExisting; - - if (directory.EnumerateFileSystemInfos().Any()) - return NewDirectoryState.ExistsNonEmpty; - - return NewDirectoryState.ExistsEmpty; + Penumbra.Log.Error($"Error reloading moved mod {mod.Name}."); + return; } + ModPathChanged.Invoke(ModPathChangeType.Moved, mod, oldDirectory, dir); + if (metaChange != ModDataChangeType.None) + _communicator.ModDataChanged.Invoke(metaChange, mod, oldName); + } - /// Add new mods to NewMods and remove deleted mods from NewMods. - private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, - DirectoryInfo? newDirectory) + /// + /// Reload a mod without changing its base directory. + /// If the base directory does not exist anymore, the mod will be deleted. + /// + public void ReloadMod(int idx) + { + var mod = this[idx]; + var oldName = mod.Name; + + ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath); + if (!mod.Reload(this, true, out var metaChange)) { - switch (type) + Penumbra.Log.Warning(mod.Name.Length == 0 + ? $"Reloading mod {oldName} has failed, new name is empty. Deleting instead." + : $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it ha. Deleting instead."); + + DeleteMod(idx); + return; + } + + ModPathChanged.Invoke(ModPathChangeType.Reloaded, mod, mod.ModPath, mod.ModPath); + if (metaChange != ModDataChangeType.None) + _communicator.ModDataChanged.Invoke(metaChange, mod, oldName); + } + + /// + /// Delete a mod by its index. The event is invoked before the mod is removed from the list. + /// Deletes from filesystem as well as from internal data. + /// Updates indices of later mods. + /// + public void DeleteMod(int idx) + { + var mod = this[idx]; + if (Directory.Exists(mod.ModPath.FullName)) + try { - case ModPathChangeType.Added: - NewMods.Add(mod); - break; - case ModPathChangeType.Deleted: - NewMods.Remove(mod); - break; - case ModPathChangeType.Moved: - if (oldDirectory != null && newDirectory != null) - DataEditor.MoveDataFile(oldDirectory, newDirectory); - - break; + Directory.Delete(mod.ModPath.FullName, true); + Penumbra.Log.Debug($"Deleted directory {mod.ModPath.FullName} for {mod.Name}."); } + catch (Exception e) + { + Penumbra.Log.Error($"Could not delete the mod {mod.ModPath.Name}:\n{e}"); + } + + ModPathChanged.Invoke(ModPathChangeType.Deleted, mod, mod.ModPath, null); + _mods.RemoveAt(idx); + foreach (var remainingMod in _mods.Skip(idx)) + --remainingMod.Index; + + Penumbra.Log.Debug($"Deleted mod {mod.Name}."); + } + + /// Load a new mod and add it to the manager if successful. + public void AddMod(DirectoryInfo modFolder) + { + if (_mods.Any(m => m.ModPath.Name == modFolder.Name)) + return; + + Mod.Creator.SplitMultiGroups(modFolder); + var mod = Mod.LoadMod(this, modFolder, true); + if (mod == null) + return; + + mod.Index = _mods.Count; + _mods.Add(mod); + ModPathChanged.Invoke(ModPathChangeType.Added, mod, null, mod.ModPath); + Penumbra.Log.Debug($"Added new mod {mod.Name} from {modFolder.FullName}."); + } + + public enum NewDirectoryState + { + NonExisting, + ExistsEmpty, + ExistsNonEmpty, + ExistsAsFile, + ContainsInvalidSymbols, + Identical, + Empty, + } + + /// Return the state of the new potential name of a directory. + public NewDirectoryState NewDirectoryValid(string oldName, string newName, out DirectoryInfo? directory) + { + directory = null; + if (newName.Length == 0) + return NewDirectoryState.Empty; + + if (oldName == newName) + return NewDirectoryState.Identical; + + var fixedNewName = Mod.Creator.ReplaceBadXivSymbols(newName); + if (fixedNewName != newName) + return NewDirectoryState.ContainsInvalidSymbols; + + directory = new DirectoryInfo(Path.Combine(BasePath.FullName, fixedNewName)); + if (File.Exists(directory.FullName)) + return NewDirectoryState.ExistsAsFile; + + if (!Directory.Exists(directory.FullName)) + return NewDirectoryState.NonExisting; + + if (directory.EnumerateFileSystemInfos().Any()) + return NewDirectoryState.ExistsNonEmpty; + + return NewDirectoryState.ExistsEmpty; + } + + + /// Add new mods to NewMods and remove deleted mods from NewMods. + private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, + DirectoryInfo? newDirectory) + { + switch (type) + { + case ModPathChangeType.Added: + NewMods.Add(mod); + break; + case ModPathChangeType.Deleted: + NewMods.Remove(mod); + break; + case ModPathChangeType.Moved: + if (oldDirectory != null && newDirectory != null) + DataEditor.MoveDataFile(oldDirectory, newDirectory); + + break; } } } diff --git a/Penumbra/Mods/Manager/Mod.Manager.Local.cs b/Penumbra/Mods/Manager/Mod.Manager.Local.cs deleted file mode 100644 index f838677f..00000000 --- a/Penumbra/Mods/Manager/Mod.Manager.Local.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Linq; - -namespace Penumbra.Mods; - -public sealed partial class Mod -{ - public partial class Manager - { - - } -} \ No newline at end of file diff --git a/Penumbra/Mods/Manager/Mod.Manager.Options.cs b/Penumbra/Mods/Manager/Mod.Manager.Options.cs index a9b11b26..0c86e82d 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Options.cs @@ -11,371 +11,367 @@ using Penumbra.Util; namespace Penumbra.Mods; -public sealed partial class Mod +public sealed partial class ModManager { - public sealed partial class Manager + public delegate void ModOptionChangeDelegate(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx); + public event ModOptionChangeDelegate ModOptionChanged; + + public void ChangeModGroupType(Mod mod, int groupIdx, GroupType type) { - public delegate void ModOptionChangeDelegate(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx); - public event ModOptionChangeDelegate ModOptionChanged; + var group = mod._groups[groupIdx]; + if (group.Type == type) + return; - public void ChangeModGroupType(Mod mod, int groupIdx, GroupType type) + mod._groups[groupIdx] = group.Convert(type); + ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1); + } + + public void ChangeModGroupDefaultOption(Mod mod, int groupIdx, uint defaultOption) + { + var group = mod._groups[groupIdx]; + if (group.DefaultSettings == defaultOption) + return; + + group.DefaultSettings = defaultOption; + ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, mod, groupIdx, -1, -1); + } + + public void RenameModGroup(Mod mod, int groupIdx, string newName) + { + var group = mod._groups[groupIdx]; + var oldName = group.Name; + if (oldName == newName || !VerifyFileName(mod, group, newName, true)) + return; + + group.DeleteFile(mod.ModPath, groupIdx); + + var _ = group switch { - var group = mod._groups[groupIdx]; - if (group.Type == type) - return; + SingleModGroup s => s.Name = newName, + MultiModGroup m => m.Name = newName, + _ => newName, + }; - mod._groups[groupIdx] = group.Convert(type); - ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1); - } + ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1); + } - public void ChangeModGroupDefaultOption(Mod mod, int groupIdx, uint defaultOption) - { - var group = mod._groups[groupIdx]; - if (group.DefaultSettings == defaultOption) - return; + public void AddModGroup(Mod mod, GroupType type, string newName) + { + if (!VerifyFileName(mod, null, newName, true)) + return; - group.DefaultSettings = defaultOption; - ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, mod, groupIdx, -1, -1); - } + var maxPriority = mod._groups.Count == 0 ? 0 : mod._groups.Max(o => o.Priority) + 1; - public void RenameModGroup(Mod mod, int groupIdx, string newName) - { - var group = mod._groups[groupIdx]; - var oldName = group.Name; - if (oldName == newName || !VerifyFileName(mod, group, newName, true)) - return; - - group.DeleteFile(mod.ModPath, groupIdx); - - var _ = group switch + mod._groups.Add(type == GroupType.Multi + ? new MultiModGroup { - SingleModGroup s => s.Name = newName, - MultiModGroup m => m.Name = newName, - _ => newName, - }; - - ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1); - } - - public void AddModGroup(Mod mod, GroupType type, string newName) - { - if (!VerifyFileName(mod, null, newName, true)) - return; - - var maxPriority = mod._groups.Count == 0 ? 0 : mod._groups.Max(o => o.Priority) + 1; - - mod._groups.Add(type == GroupType.Multi - ? new MultiModGroup - { - Name = newName, - Priority = maxPriority, - } - : new SingleModGroup - { - Name = newName, - Priority = maxPriority, - }); - ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod._groups.Count - 1, -1, -1); - } - - public void DeleteModGroup(Mod mod, int groupIdx) - { - var group = mod._groups[groupIdx]; - ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1); - mod._groups.RemoveAt(groupIdx); - UpdateSubModPositions(mod, groupIdx); - group.DeleteFile(mod.ModPath, groupIdx); - ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1); - } - - public void MoveModGroup(Mod mod, int groupIdxFrom, int groupIdxTo) - { - if (mod._groups.Move(groupIdxFrom, groupIdxTo)) - { - UpdateSubModPositions(mod, Math.Min(groupIdxFrom, groupIdxTo)); - ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo); + Name = newName, + Priority = maxPriority, } - } - - private static void UpdateSubModPositions(Mod mod, int fromGroup) - { - foreach (var (group, groupIdx) in mod._groups.WithIndex().Skip(fromGroup)) + : new SingleModGroup { - foreach (var (o, optionIdx) in group.OfType().WithIndex()) - o.SetPosition(groupIdx, optionIdx); - } - } + Name = newName, + Priority = maxPriority, + }); + ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod._groups.Count - 1, -1, -1); + } - public void ChangeGroupDescription(Mod mod, int groupIdx, string newDescription) + public void DeleteModGroup(Mod mod, int groupIdx) + { + var group = mod._groups[groupIdx]; + ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1); + mod._groups.RemoveAt(groupIdx); + UpdateSubModPositions(mod, groupIdx); + group.DeleteFile(mod.ModPath, groupIdx); + ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1); + } + + public void MoveModGroup(Mod mod, int groupIdxFrom, int groupIdxTo) + { + if (mod._groups.Move(groupIdxFrom, groupIdxTo)) { - var group = mod._groups[groupIdx]; - if (group.Description == newDescription) - return; - - var _ = group switch - { - SingleModGroup s => s.Description = newDescription, - MultiModGroup m => m.Description = newDescription, - _ => newDescription, - }; - ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, -1, -1); - } - - public void ChangeOptionDescription(Mod mod, int groupIdx, int optionIdx, string newDescription) - { - var group = mod._groups[groupIdx]; - var option = group[optionIdx]; - if (option.Description == newDescription || option is not SubMod s) - return; - - s.Description = newDescription; - ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); - } - - public void ChangeGroupPriority(Mod mod, int groupIdx, int newPriority) - { - var group = mod._groups[groupIdx]; - if (group.Priority == newPriority) - return; - - var _ = group switch - { - SingleModGroup s => s.Priority = newPriority, - MultiModGroup m => m.Priority = newPriority, - _ => newPriority, - }; - ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1); - } - - public void ChangeOptionPriority(Mod mod, int groupIdx, int optionIdx, int newPriority) - { - switch (mod._groups[groupIdx]) - { - case SingleModGroup: - ChangeGroupPriority(mod, groupIdx, newPriority); - break; - case MultiModGroup m: - if (m.PrioritizedOptions[optionIdx].Priority == newPriority) - return; - - m.PrioritizedOptions[optionIdx] = (m.PrioritizedOptions[optionIdx].Mod, newPriority); - ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1); - return; - } - } - - public void RenameOption(Mod mod, int groupIdx, int optionIdx, string newName) - { - switch (mod._groups[groupIdx]) - { - case SingleModGroup s: - if (s.OptionData[optionIdx].Name == newName) - return; - - s.OptionData[optionIdx].Name = newName; - break; - case MultiModGroup m: - var option = m.PrioritizedOptions[optionIdx].Mod; - if (option.Name == newName) - return; - - option.Name = newName; - break; - } - - ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); - } - - public void AddOption(Mod mod, int groupIdx, string newName) - { - var group = mod._groups[groupIdx]; - var subMod = new SubMod(mod) { Name = newName }; - subMod.SetPosition(groupIdx, group.Count); - switch (group) - { - case SingleModGroup s: - s.OptionData.Add(subMod); - break; - case MultiModGroup m: - m.PrioritizedOptions.Add((subMod, 0)); - break; - } - - ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); - } - - public void AddOption(Mod mod, int groupIdx, ISubMod option, int priority = 0) - { - if (option is not SubMod o) - return; - - var group = mod._groups[groupIdx]; - if (group.Count > 63) - { - Penumbra.Log.Error( - $"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, " - + "since only up to 64 options are supported in one group."); - return; - } - - o.SetPosition(groupIdx, group.Count); - - switch (group) - { - case SingleModGroup s: - s.OptionData.Add(o); - break; - case MultiModGroup m: - m.PrioritizedOptions.Add((o, priority)); - break; - } - - ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); - } - - public void DeleteOption(Mod mod, int groupIdx, int optionIdx) - { - var group = mod._groups[groupIdx]; - ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); - switch (group) - { - case SingleModGroup s: - s.OptionData.RemoveAt(optionIdx); - - break; - case MultiModGroup m: - m.PrioritizedOptions.RemoveAt(optionIdx); - break; - } - - group.UpdatePositions(optionIdx); - ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1); - } - - public void MoveOption(Mod mod, int groupIdx, int optionIdxFrom, int optionIdxTo) - { - var group = mod._groups[groupIdx]; - if (group.MoveOption(optionIdxFrom, optionIdxTo)) - ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, mod, groupIdx, optionIdxFrom, optionIdxTo); - } - - public void OptionSetManipulations(Mod mod, int groupIdx, int optionIdx, HashSet manipulations) - { - var subMod = GetSubMod(mod, groupIdx, optionIdx); - if (subMod.Manipulations.Count == manipulations.Count - && subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m))) - return; - - ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); - subMod.ManipulationData = manipulations; - ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1); - } - - public void OptionSetFiles(Mod mod, int groupIdx, int optionIdx, Dictionary replacements) - { - var subMod = GetSubMod(mod, groupIdx, optionIdx); - if (subMod.FileData.SetEquals(replacements)) - return; - - ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); - subMod.FileData = replacements; - ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1); - } - - public void OptionAddFiles(Mod mod, int groupIdx, int optionIdx, Dictionary additions) - { - var subMod = GetSubMod(mod, groupIdx, optionIdx); - var oldCount = subMod.FileData.Count; - subMod.FileData.AddFrom(additions); - if (oldCount != subMod.FileData.Count) - ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, optionIdx, -1); - } - - public void OptionSetFileSwaps(Mod mod, int groupIdx, int optionIdx, Dictionary swaps) - { - var subMod = GetSubMod(mod, groupIdx, optionIdx); - if (subMod.FileSwapData.SetEquals(swaps)) - return; - - ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); - subMod.FileSwapData = swaps; - ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1); - } - - public bool VerifyFileName(Mod mod, IModGroup? group, string newName, bool message) - { - var path = newName.RemoveInvalidPathSymbols(); - if (path.Length != 0 - && !mod.Groups.Any(o => !ReferenceEquals(o, group) - && string.Equals(o.Name.RemoveInvalidPathSymbols(), path, StringComparison.OrdinalIgnoreCase))) - return true; - - if (message) - Penumbra.ChatService.NotificationMessage( - $"Could not name option {newName} because option with same filename {path} already exists.", - "Warning", NotificationType.Warning); - - return false; - - } - - private static SubMod GetSubMod(Mod mod, int groupIdx, int optionIdx) - { - if (groupIdx == -1 && optionIdx == 0) - return mod._default; - - return mod._groups[groupIdx] switch - { - SingleModGroup s => s.OptionData[optionIdx], - MultiModGroup m => m.PrioritizedOptions[optionIdx].Mod, - _ => throw new InvalidOperationException(), - }; - } - - private static void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int _, int _2) - { - if (type == ModOptionChangeType.PrepareChange) - return; - - // File deletion is handled in the actual function. - if (type is ModOptionChangeType.GroupDeleted or ModOptionChangeType.GroupMoved) - { - mod.SaveAllGroups(); - } - else - { - if (groupIdx == -1) - mod.SaveDefaultModDelayed(); - else - IModGroup.SaveDelayed(mod._groups[groupIdx], mod.ModPath, groupIdx); - } - - bool ComputeChangedItems() - { - mod.ComputeChangedItems(); - return true; - } - - // State can not change on adding groups, as they have no immediate options. - var unused = type switch - { - ModOptionChangeType.GroupAdded => ComputeChangedItems() & mod.SetCounts(), - ModOptionChangeType.GroupDeleted => ComputeChangedItems() & mod.SetCounts(), - ModOptionChangeType.GroupMoved => false, - ModOptionChangeType.GroupTypeChanged => mod.HasOptions = mod.Groups.Any(o => o.IsOption), - ModOptionChangeType.PriorityChanged => false, - ModOptionChangeType.OptionAdded => ComputeChangedItems() & mod.SetCounts(), - ModOptionChangeType.OptionDeleted => ComputeChangedItems() & mod.SetCounts(), - ModOptionChangeType.OptionMoved => false, - ModOptionChangeType.OptionFilesChanged => ComputeChangedItems() - & (0 < (mod.TotalFileCount = mod.AllSubMods.Sum(s => s.Files.Count))), - ModOptionChangeType.OptionSwapsChanged => ComputeChangedItems() - & (0 < (mod.TotalSwapCount = mod.AllSubMods.Sum(s => s.FileSwaps.Count))), - ModOptionChangeType.OptionMetaChanged => ComputeChangedItems() - & (0 < (mod.TotalManipulations = mod.AllSubMods.Sum(s => s.Manipulations.Count))), - ModOptionChangeType.DisplayChange => false, - _ => false, - }; + UpdateSubModPositions(mod, Math.Min(groupIdxFrom, groupIdxTo)); + ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo); } } + + private static void UpdateSubModPositions(Mod mod, int fromGroup) + { + foreach (var (group, groupIdx) in mod._groups.WithIndex().Skip(fromGroup)) + { + foreach (var (o, optionIdx) in group.OfType().WithIndex()) + o.SetPosition(groupIdx, optionIdx); + } + } + + public void ChangeGroupDescription(Mod mod, int groupIdx, string newDescription) + { + var group = mod._groups[groupIdx]; + if (group.Description == newDescription) + return; + + var _ = group switch + { + SingleModGroup s => s.Description = newDescription, + MultiModGroup m => m.Description = newDescription, + _ => newDescription, + }; + ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, -1, -1); + } + + public void ChangeOptionDescription(Mod mod, int groupIdx, int optionIdx, string newDescription) + { + var group = mod._groups[groupIdx]; + var option = group[optionIdx]; + if (option.Description == newDescription || option is not SubMod s) + return; + + s.Description = newDescription; + ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); + } + + public void ChangeGroupPriority(Mod mod, int groupIdx, int newPriority) + { + var group = mod._groups[groupIdx]; + if (group.Priority == newPriority) + return; + + var _ = group switch + { + SingleModGroup s => s.Priority = newPriority, + MultiModGroup m => m.Priority = newPriority, + _ => newPriority, + }; + ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1); + } + + public void ChangeOptionPriority(Mod mod, int groupIdx, int optionIdx, int newPriority) + { + switch (mod._groups[groupIdx]) + { + case SingleModGroup: + ChangeGroupPriority(mod, groupIdx, newPriority); + break; + case MultiModGroup m: + if (m.PrioritizedOptions[optionIdx].Priority == newPriority) + return; + + m.PrioritizedOptions[optionIdx] = (m.PrioritizedOptions[optionIdx].Mod, newPriority); + ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1); + return; + } + } + + public void RenameOption(Mod mod, int groupIdx, int optionIdx, string newName) + { + switch (mod._groups[groupIdx]) + { + case SingleModGroup s: + if (s.OptionData[optionIdx].Name == newName) + return; + + s.OptionData[optionIdx].Name = newName; + break; + case MultiModGroup m: + var option = m.PrioritizedOptions[optionIdx].Mod; + if (option.Name == newName) + return; + + option.Name = newName; + break; + } + + ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); + } + + public void AddOption(Mod mod, int groupIdx, string newName) + { + var group = mod._groups[groupIdx]; + var subMod = new SubMod(mod) { Name = newName }; + subMod.SetPosition(groupIdx, group.Count); + switch (group) + { + case SingleModGroup s: + s.OptionData.Add(subMod); + break; + case MultiModGroup m: + m.PrioritizedOptions.Add((subMod, 0)); + break; + } + + ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); + } + + public void AddOption(Mod mod, int groupIdx, ISubMod option, int priority = 0) + { + if (option is not SubMod o) + return; + + var group = mod._groups[groupIdx]; + if (group.Count > 63) + { + Penumbra.Log.Error( + $"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, " + + "since only up to 64 options are supported in one group."); + return; + } + + o.SetPosition(groupIdx, group.Count); + + switch (group) + { + case SingleModGroup s: + s.OptionData.Add(o); + break; + case MultiModGroup m: + m.PrioritizedOptions.Add((o, priority)); + break; + } + + ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); + } + + public void DeleteOption(Mod mod, int groupIdx, int optionIdx) + { + var group = mod._groups[groupIdx]; + ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); + switch (group) + { + case SingleModGroup s: + s.OptionData.RemoveAt(optionIdx); + + break; + case MultiModGroup m: + m.PrioritizedOptions.RemoveAt(optionIdx); + break; + } + + group.UpdatePositions(optionIdx); + ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1); + } + + public void MoveOption(Mod mod, int groupIdx, int optionIdxFrom, int optionIdxTo) + { + var group = mod._groups[groupIdx]; + if (group.MoveOption(optionIdxFrom, optionIdxTo)) + ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, mod, groupIdx, optionIdxFrom, optionIdxTo); + } + + public void OptionSetManipulations(Mod mod, int groupIdx, int optionIdx, HashSet manipulations) + { + var subMod = GetSubMod(mod, groupIdx, optionIdx); + if (subMod.Manipulations.Count == manipulations.Count + && subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m))) + return; + + ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); + subMod.ManipulationData = manipulations; + ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1); + } + + public void OptionSetFiles(Mod mod, int groupIdx, int optionIdx, Dictionary replacements) + { + var subMod = GetSubMod(mod, groupIdx, optionIdx); + if (subMod.FileData.SetEquals(replacements)) + return; + + ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); + subMod.FileData = replacements; + ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1); + } + + public void OptionAddFiles(Mod mod, int groupIdx, int optionIdx, Dictionary additions) + { + var subMod = GetSubMod(mod, groupIdx, optionIdx); + var oldCount = subMod.FileData.Count; + subMod.FileData.AddFrom(additions); + if (oldCount != subMod.FileData.Count) + ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, optionIdx, -1); + } + + public void OptionSetFileSwaps(Mod mod, int groupIdx, int optionIdx, Dictionary swaps) + { + var subMod = GetSubMod(mod, groupIdx, optionIdx); + if (subMod.FileSwapData.SetEquals(swaps)) + return; + + ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); + subMod.FileSwapData = swaps; + ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1); + } + + public bool VerifyFileName(Mod mod, IModGroup? group, string newName, bool message) + { + var path = newName.RemoveInvalidPathSymbols(); + if (path.Length != 0 + && !mod.Groups.Any(o => !ReferenceEquals(o, group) + && string.Equals(o.Name.RemoveInvalidPathSymbols(), path, StringComparison.OrdinalIgnoreCase))) + return true; + + if (message) + Penumbra.ChatService.NotificationMessage( + $"Could not name option {newName} because option with same filename {path} already exists.", + "Warning", NotificationType.Warning); + + return false; + } + + private static SubMod GetSubMod(Mod mod, int groupIdx, int optionIdx) + { + if (groupIdx == -1 && optionIdx == 0) + return mod._default; + + return mod._groups[groupIdx] switch + { + SingleModGroup s => s.OptionData[optionIdx], + MultiModGroup m => m.PrioritizedOptions[optionIdx].Mod, + _ => throw new InvalidOperationException(), + }; + } + + private static void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int _, int _2) + { + if (type == ModOptionChangeType.PrepareChange) + return; + + // File deletion is handled in the actual function. + if (type is ModOptionChangeType.GroupDeleted or ModOptionChangeType.GroupMoved) + { + mod.SaveAllGroups(); + } + else + { + if (groupIdx == -1) + mod.SaveDefaultModDelayed(); + else + IModGroup.SaveDelayed(mod._groups[groupIdx], mod.ModPath, groupIdx); + } + + bool ComputeChangedItems() + { + mod.ComputeChangedItems(); + return true; + } + + // State can not change on adding groups, as they have no immediate options. + var unused = type switch + { + ModOptionChangeType.GroupAdded => ComputeChangedItems() & mod.SetCounts(), + ModOptionChangeType.GroupDeleted => ComputeChangedItems() & mod.SetCounts(), + ModOptionChangeType.GroupMoved => false, + ModOptionChangeType.GroupTypeChanged => mod.HasOptions = mod.Groups.Any(o => o.IsOption), + ModOptionChangeType.PriorityChanged => false, + ModOptionChangeType.OptionAdded => ComputeChangedItems() & mod.SetCounts(), + ModOptionChangeType.OptionDeleted => ComputeChangedItems() & mod.SetCounts(), + ModOptionChangeType.OptionMoved => false, + ModOptionChangeType.OptionFilesChanged => ComputeChangedItems() + & (0 < (mod.TotalFileCount = mod.AllSubMods.Sum(s => s.Files.Count))), + ModOptionChangeType.OptionSwapsChanged => ComputeChangedItems() + & (0 < (mod.TotalSwapCount = mod.AllSubMods.Sum(s => s.FileSwaps.Count))), + ModOptionChangeType.OptionMetaChanged => ComputeChangedItems() + & (0 < (mod.TotalManipulations = mod.AllSubMods.Sum(s => s.Manipulations.Count))), + ModOptionChangeType.DisplayChange => false, + _ => false, + }; + } } diff --git a/Penumbra/Mods/Manager/Mod.Manager.Root.cs b/Penumbra/Mods/Manager/Mod.Manager.Root.cs index 79dd780b..0377e09b 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Root.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Root.cs @@ -6,169 +6,144 @@ using System.Threading.Tasks; namespace Penumbra.Mods; -public sealed partial class Mod +public sealed partial class ModManager { - public sealed partial class Manager + public DirectoryInfo BasePath { get; private set; } = null!; + private DirectoryInfo? _exportDirectory; + + public DirectoryInfo ExportDirectory + => _exportDirectory ?? BasePath; + + public bool Valid { get; private set; } + + public event Action? ModDiscoveryStarted; + public event Action? ModDiscoveryFinished; + public event Action ModDirectoryChanged; + + // Change the mod base directory and discover available mods. + public void DiscoverMods(string newDir) { - public DirectoryInfo BasePath { get; private set; } = null!; - private DirectoryInfo? _exportDirectory; + SetBaseDirectory(newDir, false); + DiscoverMods(); + } - public DirectoryInfo ExportDirectory - => _exportDirectory ?? BasePath; + // Set the mod base directory. + // If its not the first time, check if it is the same directory as before. + // Also checks if the directory is available and tries to create it if it is not. + private void SetBaseDirectory(string newPath, bool firstTime) + { + if (!firstTime && string.Equals(newPath, Penumbra.Config.ModDirectory, StringComparison.OrdinalIgnoreCase)) + return; - public bool Valid { get; private set; } - - public event Action? ModDiscoveryStarted; - public event Action? ModDiscoveryFinished; - public event Action< string, bool > ModDirectoryChanged; - - // Change the mod base directory and discover available mods. - public void DiscoverMods( string newDir ) + if (newPath.Length == 0) { - SetBaseDirectory( newDir, false ); - DiscoverMods(); + Valid = false; + BasePath = new DirectoryInfo("."); + if (Penumbra.Config.ModDirectory != BasePath.FullName) + ModDirectoryChanged.Invoke(string.Empty, false); } - - // Set the mod base directory. - // If its not the first time, check if it is the same directory as before. - // Also checks if the directory is available and tries to create it if it is not. - private void SetBaseDirectory( string newPath, bool firstTime ) + else { - if( !firstTime && string.Equals( newPath, Penumbra.Config.ModDirectory, StringComparison.OrdinalIgnoreCase ) ) - { - return; - } - - if( newPath.Length == 0 ) - { - Valid = false; - BasePath = new DirectoryInfo( "." ); - if( Penumbra.Config.ModDirectory != BasePath.FullName ) - { - ModDirectoryChanged.Invoke( string.Empty, false ); - } - } - else - { - var newDir = new DirectoryInfo( newPath ); - if( !newDir.Exists ) - { - try - { - Directory.CreateDirectory( newDir.FullName ); - newDir.Refresh(); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not create specified mod directory {newDir.FullName}:\n{e}" ); - } - } - - BasePath = newDir; - Valid = Directory.Exists( newDir.FullName ); - if( Penumbra.Config.ModDirectory != BasePath.FullName ) - { - ModDirectoryChanged.Invoke( BasePath.FullName, Valid ); - } - } - } - - private static void OnModDirectoryChange( string newPath, bool _ ) - { - Penumbra.Log.Information( $"Set new mod base directory from {Penumbra.Config.ModDirectory} to {newPath}." ); - Penumbra.Config.ModDirectory = newPath; - Penumbra.Config.Save(); - } - - // Discover new mods. - public void DiscoverMods() - { - NewMods.Clear(); - ModDiscoveryStarted?.Invoke(); - _mods.Clear(); - BasePath.Refresh(); - - if( Valid && BasePath.Exists ) - { - var options = new ParallelOptions() - { - MaxDegreeOfParallelism = Environment.ProcessorCount / 2, - }; - var queue = new ConcurrentQueue< Mod >(); - Parallel.ForEach( BasePath.EnumerateDirectories(), options, dir => - { - var mod = LoadMod( this, dir, false ); - if( mod != null ) - { - queue.Enqueue( mod ); - } - } ); - - foreach( var mod in queue ) - { - mod.Index = _mods.Count; - _mods.Add( mod ); - } - } - - ModDiscoveryFinished?.Invoke(); - Penumbra.Log.Information( "Rediscovered mods." ); - - if( MigrateModBackups ) - { - ModBackup.MigrateZipToPmp( this ); - } - } - - public void UpdateExportDirectory( string newDirectory, bool change ) - { - if( newDirectory.Length == 0 ) - { - if( _exportDirectory == null ) - { - return; - } - - _exportDirectory = null; - _config.ExportDirectory = string.Empty; - _config.Save(); - return; - } - - var dir = new DirectoryInfo( newDirectory ); - if( dir.FullName.Equals( _exportDirectory?.FullName, StringComparison.OrdinalIgnoreCase ) ) - { - return; - } - - if( !dir.Exists ) - { + var newDir = new DirectoryInfo(newPath); + if (!newDir.Exists) try { - Directory.CreateDirectory( dir.FullName ); + Directory.CreateDirectory(newDir.FullName); + newDir.Refresh(); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Could not create Export Directory:\n{e}" ); - return; + Penumbra.Log.Error($"Could not create specified mod directory {newDir.FullName}:\n{e}"); } - } - if( change ) - { - foreach( var mod in _mods ) - { - new ModBackup( this, mod ).Move( dir.FullName ); - } - } - - _exportDirectory = dir; - - if( change ) - { - _config.ExportDirectory = dir.FullName; - _config.Save(); - } + BasePath = newDir; + Valid = Directory.Exists(newDir.FullName); + if (Penumbra.Config.ModDirectory != BasePath.FullName) + ModDirectoryChanged.Invoke(BasePath.FullName, Valid); } } -} \ No newline at end of file + + private static void OnModDirectoryChange(string newPath, bool _) + { + Penumbra.Log.Information($"Set new mod base directory from {Penumbra.Config.ModDirectory} to {newPath}."); + Penumbra.Config.ModDirectory = newPath; + Penumbra.Config.Save(); + } + + // Discover new mods. + public void DiscoverMods() + { + NewMods.Clear(); + ModDiscoveryStarted?.Invoke(); + _mods.Clear(); + BasePath.Refresh(); + + if (Valid && BasePath.Exists) + { + var options = new ParallelOptions() + { + MaxDegreeOfParallelism = Environment.ProcessorCount / 2, + }; + var queue = new ConcurrentQueue(); + Parallel.ForEach(BasePath.EnumerateDirectories(), options, dir => + { + var mod = Mod.LoadMod(this, dir, false); + if (mod != null) + queue.Enqueue(mod); + }); + + foreach (var mod in queue) + { + mod.Index = _mods.Count; + _mods.Add(mod); + } + } + + ModDiscoveryFinished?.Invoke(); + Penumbra.Log.Information("Rediscovered mods."); + + if (MigrateModBackups) + ModBackup.MigrateZipToPmp(this); + } + + public void UpdateExportDirectory(string newDirectory, bool change) + { + if (newDirectory.Length == 0) + { + if (_exportDirectory == null) + return; + + _exportDirectory = null; + _config.ExportDirectory = string.Empty; + _config.Save(); + return; + } + + var dir = new DirectoryInfo(newDirectory); + if (dir.FullName.Equals(_exportDirectory?.FullName, StringComparison.OrdinalIgnoreCase)) + return; + + if (!dir.Exists) + try + { + Directory.CreateDirectory(dir.FullName); + } + catch (Exception e) + { + Penumbra.Log.Error($"Could not create Export Directory:\n{e}"); + return; + } + + if (change) + foreach (var mod in _mods) + new ModBackup(this, mod).Move(dir.FullName); + + _exportDirectory = dir; + + if (change) + { + _config.ExportDirectory = dir.FullName; + _config.Save(); + } + } +} diff --git a/Penumbra/Mods/Manager/Mod.Manager.cs b/Penumbra/Mods/Manager/Mod.Manager.cs index 0e47f3ae..dd40e9d4 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.cs @@ -7,73 +7,70 @@ using Penumbra.Util; namespace Penumbra.Mods; -public sealed partial class Mod +public sealed partial class ModManager : IReadOnlyList { - public sealed partial class Manager : IReadOnlyList + // Set when reading Config and migrating from v4 to v5. + public static bool MigrateModBackups = false; + + // An easily accessible set of new mods. + // Mods are added when they are created or imported. + // Mods are removed when they are deleted or when they are toggled in any collection. + // Also gets cleared on mod rediscovery. + public readonly HashSet NewMods = new(); + + private readonly List _mods = new(); + + public Mod this[int idx] + => _mods[idx]; + + public Mod this[Index idx] + => _mods[idx]; + + public int Count + => _mods.Count; + + public IEnumerator GetEnumerator() + => _mods.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + private readonly Configuration _config; + private readonly CommunicatorService _communicator; + public readonly ModDataEditor DataEditor; + + public ModManager(StartTracker time, Configuration config, CommunicatorService communicator, ModDataEditor dataEditor) { - // Set when reading Config and migrating from v4 to v5. - public static bool MigrateModBackups = false; + using var timer = time.Measure(StartTimeType.Mods); + _config = config; + _communicator = communicator; + DataEditor = dataEditor; + ModDirectoryChanged += OnModDirectoryChange; + SetBaseDirectory(config.ModDirectory, true); + UpdateExportDirectory(_config.ExportDirectory, false); + ModOptionChanged += OnModOptionChange; + ModPathChanged += OnModPathChange; + DiscoverMods(); + } - // An easily accessible set of new mods. - // Mods are added when they are created or imported. - // Mods are removed when they are deleted or when they are toggled in any collection. - // Also gets cleared on mod rediscovery. - public readonly HashSet NewMods = new(); - private readonly List _mods = new(); - - public Mod this[int idx] - => _mods[idx]; - - public Mod this[Index idx] - => _mods[idx]; - - public int Count - => _mods.Count; - - public IEnumerator GetEnumerator() - => _mods.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - private readonly Configuration _config; - private readonly CommunicatorService _communicator; - public readonly ModDataEditor DataEditor; - - public Manager(StartTracker time, Configuration config, CommunicatorService communicator, ModDataEditor dataEditor) + // Try to obtain a mod by its directory name (unique identifier, preferred), + // or the first mod of the given name if no directory fits. + public bool TryGetMod(string modDirectory, string modName, [NotNullWhen(true)] out Mod? mod) + { + mod = null; + foreach (var m in _mods) { - using var timer = time.Measure(StartTimeType.Mods); - _config = config; - _communicator = communicator; - DataEditor = dataEditor; - ModDirectoryChanged += OnModDirectoryChange; - SetBaseDirectory(config.ModDirectory, true); - UpdateExportDirectory(_config.ExportDirectory, false); - ModOptionChanged += OnModOptionChange; - ModPathChanged += OnModPathChange; - DiscoverMods(); - } - - - // Try to obtain a mod by its directory name (unique identifier, preferred), - // or the first mod of the given name if no directory fits. - public bool TryGetMod(string modDirectory, string modName, [NotNullWhen(true)] out Mod? mod) - { - mod = null; - foreach (var m in _mods) + if (string.Equals(m.ModPath.Name, modDirectory, StringComparison.OrdinalIgnoreCase)) { - if (string.Equals(m.ModPath.Name, modDirectory, StringComparison.OrdinalIgnoreCase)) - { - mod = m; - return true; - } - - if (m.Name == modName) - mod ??= m; + mod = m; + return true; } - return mod != null; + if (m.Name == modName) + mod ??= m; } + + return mod != null; } } diff --git a/Penumbra/Mods/Manager/ModDataEditor.cs b/Penumbra/Mods/Manager/ModDataEditor.cs index 2f236b5f..09a24bba 100644 --- a/Penumbra/Mods/Manager/ModDataEditor.cs +++ b/Penumbra/Mods/Manager/ModDataEditor.cs @@ -2,7 +2,6 @@ using System; using System.IO; using System.Linq; using Dalamud.Utility; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OtterGui.Classes; using Penumbra.Services; diff --git a/Penumbra/Mods/Mod.BasePath.cs b/Penumbra/Mods/Mod.BasePath.cs index c7367a68..fb71ade6 100644 --- a/Penumbra/Mods/Mod.BasePath.cs +++ b/Penumbra/Mods/Mod.BasePath.cs @@ -15,10 +15,10 @@ public enum ModPathChangeType public partial class Mod { - public DirectoryInfo ModPath { get; private set; } + public DirectoryInfo ModPath { get; internal set; } public string Identifier => Index >= 0 ? ModPath.Name : Name; - public int Index { get; private set; } = -1; + public int Index { get; internal set; } = -1; public bool IsTemporary => Index < 0; @@ -33,7 +33,7 @@ public partial class Mod _default = new SubMod( this ); } - private static Mod? LoadMod( Manager modManager, DirectoryInfo modPath, bool incorporateMetaChanges ) + public static Mod? LoadMod( ModManager modManager, DirectoryInfo modPath, bool incorporateMetaChanges ) { modPath.Refresh(); if( !modPath.Exists ) @@ -52,7 +52,7 @@ public partial class Mod } - internal bool Reload(Manager modManager, bool incorporateMetaChanges, out ModDataChangeType modDataChange ) + internal bool Reload(ModManager modManager, bool incorporateMetaChanges, out ModDataChangeType modDataChange ) { modDataChange = ModDataChangeType.Deletion; ModPath.Refresh(); diff --git a/Penumbra/Mods/Mod.ChangedItems.cs b/Penumbra/Mods/Mod.ChangedItems.cs index 7c066681..cdffcee9 100644 --- a/Penumbra/Mods/Mod.ChangedItems.cs +++ b/Penumbra/Mods/Mod.ChangedItems.cs @@ -12,7 +12,7 @@ public sealed partial class Mod public SortedList< string, object? > ChangedItems { get; } = new(); public string LowerChangedItemsString { get; private set; } = string.Empty; - private void ComputeChangedItems() + internal void ComputeChangedItems() { ChangedItems.Clear(); foreach( var gamePath in AllRedirects ) diff --git a/Penumbra/Mods/Mod.Files.cs b/Penumbra/Mods/Mod.Files.cs index a12b29fc..6091ba1b 100644 --- a/Penumbra/Mods/Mod.Files.cs +++ b/Penumbra/Mods/Mod.Files.cs @@ -18,15 +18,15 @@ public partial class Mod public IReadOnlyList< IModGroup > Groups => _groups; - private readonly SubMod _default; - private readonly List< IModGroup > _groups = new(); + internal readonly SubMod _default; + internal readonly List< IModGroup > _groups = new(); - public int TotalFileCount { get; private set; } - public int TotalSwapCount { get; private set; } - public int TotalManipulations { get; private set; } - public bool HasOptions { get; private set; } + public int TotalFileCount { get; internal set; } + public int TotalSwapCount { get; internal set; } + public int TotalManipulations { get; internal set; } + public bool HasOptions { get; internal set; } - private bool SetCounts() + internal bool SetCounts() { TotalFileCount = 0; TotalSwapCount = 0; @@ -120,7 +120,7 @@ public partial class Mod // Delete all existing group files and save them anew. // Used when indices change in complex ways. - private void SaveAllGroups() + internal void SaveAllGroups() { foreach( var file in GroupFiles ) { diff --git a/Penumbra/Mods/ModFileSystem.cs b/Penumbra/Mods/ModFileSystem.cs index 9bd7488a..c16c172e 100644 --- a/Penumbra/Mods/ModFileSystem.cs +++ b/Penumbra/Mods/ModFileSystem.cs @@ -12,12 +12,12 @@ namespace Penumbra.Mods; public sealed class ModFileSystem : FileSystem, IDisposable, ISavable { - private readonly Mod.Manager _modManager; + private readonly ModManager _modManager; private readonly CommunicatorService _communicator; private readonly FilenameService _files; // Create a new ModFileSystem from the currently loaded mods and the current sort order file. - public ModFileSystem(Mod.Manager modManager, CommunicatorService communicator, FilenameService files) + public ModFileSystem(ModManager modManager, CommunicatorService communicator, FilenameService files) { _modManager = modManager; _communicator = communicator; diff --git a/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs b/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs index 70840b24..44314290 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs @@ -15,103 +15,105 @@ namespace Penumbra.Mods; public partial class Mod { - // Groups that allow all available options to be selected at once. - private sealed class MultiModGroup : IModGroup + +} + +/// Groups that allow all available options to be selected at once. +public sealed class MultiModGroup : IModGroup +{ + public GroupType Type + => GroupType.Multi; + + public string Name { get; set; } = "Group"; + public string Description { get; set; } = "A non-exclusive group of settings."; + public int Priority { get; set; } + public uint DefaultSettings { get; set; } + + public int OptionPriority(Index idx) + => PrioritizedOptions[idx].Priority; + + public ISubMod this[Index idx] + => PrioritizedOptions[idx].Mod; + + [JsonIgnore] + public int Count + => PrioritizedOptions.Count; + + public readonly List<(SubMod Mod, int Priority)> PrioritizedOptions = new(); + + public IEnumerator GetEnumerator() + => PrioritizedOptions.Select(o => o.Mod).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public static MultiModGroup? Load(Mod mod, JObject json, int groupIdx) { - public GroupType Type - => GroupType.Multi; - - public string Name { get; set; } = "Group"; - public string Description { get; set; } = "A non-exclusive group of settings."; - public int Priority { get; set; } - public uint DefaultSettings { get; set; } - - public int OptionPriority(Index idx) - => PrioritizedOptions[idx].Priority; - - public ISubMod this[Index idx] - => PrioritizedOptions[idx].Mod; - - [JsonIgnore] - public int Count - => PrioritizedOptions.Count; - - public readonly List<(SubMod Mod, int Priority)> PrioritizedOptions = new(); - - public IEnumerator GetEnumerator() - => PrioritizedOptions.Select(o => o.Mod).GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public static MultiModGroup? Load(Mod mod, JObject json, int groupIdx) + var ret = new MultiModGroup() { - var ret = new MultiModGroup() + Name = json[nameof(Name)]?.ToObject() ?? string.Empty, + Description = json[nameof(Description)]?.ToObject() ?? string.Empty, + Priority = json[nameof(Priority)]?.ToObject() ?? 0, + DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? 0, + }; + if (ret.Name.Length == 0) + return null; + + var options = json["Options"]; + if (options != null) + foreach (var child in options.Children()) { - Name = json[nameof(Name)]?.ToObject() ?? string.Empty, - Description = json[nameof(Description)]?.ToObject() ?? string.Empty, - Priority = json[nameof(Priority)]?.ToObject() ?? 0, - DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? 0, - }; - if (ret.Name.Length == 0) - return null; - - var options = json["Options"]; - if (options != null) - foreach (var child in options.Children()) + if (ret.PrioritizedOptions.Count == IModGroup.MaxMultiOptions) { - if (ret.PrioritizedOptions.Count == IModGroup.MaxMultiOptions) - { - Penumbra.ChatService.NotificationMessage( - $"Multi Group {ret.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options.", "Warning", - NotificationType.Warning); - break; - } - - var subMod = new SubMod(mod); - subMod.SetPosition(groupIdx, ret.PrioritizedOptions.Count); - subMod.Load(mod.ModPath, child, out var priority); - ret.PrioritizedOptions.Add((subMod, priority)); + Penumbra.ChatService.NotificationMessage( + $"Multi Group {ret.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options.", "Warning", + NotificationType.Warning); + break; } - ret.DefaultSettings = (uint)(ret.DefaultSettings & ((1ul << ret.Count) - 1)); - - return ret; - } - - public IModGroup Convert(GroupType type) - { - switch (type) - { - case GroupType.Multi: return this; - case GroupType.Single: - var multi = new SingleModGroup() - { - Name = Name, - Description = Description, - Priority = Priority, - DefaultSettings = (uint)Math.Max(Math.Min(Count - 1, BitOperations.TrailingZeroCount(DefaultSettings)), 0), - }; - multi.OptionData.AddRange(PrioritizedOptions.Select(p => p.Mod)); - return multi; - default: throw new ArgumentOutOfRangeException(nameof(type), type, null); + var subMod = new SubMod(mod); + subMod.SetPosition(groupIdx, ret.PrioritizedOptions.Count); + subMod.Load(mod.ModPath, child, out var priority); + ret.PrioritizedOptions.Add((subMod, priority)); } - } - public bool MoveOption(int optionIdxFrom, int optionIdxTo) + ret.DefaultSettings = (uint)(ret.DefaultSettings & ((1ul << ret.Count) - 1)); + + return ret; + } + + public IModGroup Convert(GroupType type) + { + switch (type) { - if (!PrioritizedOptions.Move(optionIdxFrom, optionIdxTo)) - return false; - - DefaultSettings = Functions.MoveBit(DefaultSettings, optionIdxFrom, optionIdxTo); - UpdatePositions(Math.Min(optionIdxFrom, optionIdxTo)); - return true; - } - - public void UpdatePositions(int from = 0) - { - foreach (var ((o, _), i) in PrioritizedOptions.WithIndex().Skip(from)) - o.SetPosition(o.GroupIdx, i); + case GroupType.Multi: return this; + case GroupType.Single: + var multi = new SingleModGroup() + { + Name = Name, + Description = Description, + Priority = Priority, + DefaultSettings = (uint)Math.Max(Math.Min(Count - 1, BitOperations.TrailingZeroCount(DefaultSettings)), 0), + }; + multi.OptionData.AddRange(PrioritizedOptions.Select(p => p.Mod)); + return multi; + default: throw new ArgumentOutOfRangeException(nameof(type), type, null); } } + + public bool MoveOption(int optionIdxFrom, int optionIdxTo) + { + if (!PrioritizedOptions.Move(optionIdxFrom, optionIdxTo)) + return false; + + DefaultSettings = Functions.MoveBit(DefaultSettings, optionIdxFrom, optionIdxTo); + UpdatePositions(Math.Min(optionIdxFrom, optionIdxTo)); + return true; + } + + public void UpdatePositions(int from = 0) + { + foreach (var ((o, _), i) in PrioritizedOptions.WithIndex().Skip(from)) + o.SetPosition(o.GroupIdx, i); + } } diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs b/Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs index cfec230e..b330c00d 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SingleModGroup.cs @@ -10,122 +10,119 @@ using Penumbra.Api.Enums; namespace Penumbra.Mods; -public partial class Mod +/// Groups that allow only one of their available options to be selected. +public sealed class SingleModGroup : IModGroup { - // Groups that allow only one of their available options to be selected. - private sealed class SingleModGroup : IModGroup + public GroupType Type + => GroupType.Single; + + public string Name { get; set; } = "Option"; + public string Description { get; set; } = "A mutually exclusive group of settings."; + public int Priority { get; set; } + public uint DefaultSettings { get; set; } + + public readonly List< SubMod > OptionData = new(); + + public int OptionPriority( Index _ ) + => Priority; + + public ISubMod this[ Index idx ] + => OptionData[ idx ]; + + [JsonIgnore] + public int Count + => OptionData.Count; + + public IEnumerator< ISubMod > GetEnumerator() + => OptionData.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public static SingleModGroup? Load( Mod mod, JObject json, int groupIdx ) { - public GroupType Type - => GroupType.Single; - - public string Name { get; set; } = "Option"; - public string Description { get; set; } = "A mutually exclusive group of settings."; - public int Priority { get; set; } - public uint DefaultSettings { get; set; } - - public readonly List< SubMod > OptionData = new(); - - public int OptionPriority( Index _ ) - => Priority; - - public ISubMod this[ Index idx ] - => OptionData[ idx ]; - - [JsonIgnore] - public int Count - => OptionData.Count; - - public IEnumerator< ISubMod > GetEnumerator() - => OptionData.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public static SingleModGroup? Load( Mod mod, JObject json, int groupIdx ) + var options = json[ "Options" ]; + var ret = new SingleModGroup { - var options = json[ "Options" ]; - var ret = new SingleModGroup - { - Name = json[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty, - Description = json[ nameof( Description ) ]?.ToObject< string >() ?? string.Empty, - Priority = json[ nameof( Priority ) ]?.ToObject< int >() ?? 0, - DefaultSettings = json[ nameof( DefaultSettings ) ]?.ToObject< uint >() ?? 0u, - }; - if( ret.Name.Length == 0 ) - { - return null; - } + Name = json[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty, + Description = json[ nameof( Description ) ]?.ToObject< string >() ?? string.Empty, + Priority = json[ nameof( Priority ) ]?.ToObject< int >() ?? 0, + DefaultSettings = json[ nameof( DefaultSettings ) ]?.ToObject< uint >() ?? 0u, + }; + if( ret.Name.Length == 0 ) + { + return null; + } - if( options != null ) + if( options != null ) + { + foreach( var child in options.Children() ) { - foreach( var child in options.Children() ) + var subMod = new SubMod( mod ); + subMod.SetPosition( groupIdx, ret.OptionData.Count ); + subMod.Load( mod.ModPath, child, out _ ); + ret.OptionData.Add( subMod ); + } + } + + if( ( int )ret.DefaultSettings >= ret.Count ) + ret.DefaultSettings = 0; + + return ret; + } + + public IModGroup Convert( GroupType type ) + { + switch( type ) + { + case GroupType.Single: return this; + case GroupType.Multi: + var multi = new MultiModGroup() { - var subMod = new SubMod( mod ); - subMod.SetPosition( groupIdx, ret.OptionData.Count ); - subMod.Load( mod.ModPath, child, out _ ); - ret.OptionData.Add( subMod ); - } - } + Name = Name, + Description = Description, + Priority = Priority, + DefaultSettings = 1u << ( int )DefaultSettings, + }; + multi.PrioritizedOptions.AddRange( OptionData.Select( ( o, i ) => ( o, i ) ) ); + return multi; + default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); + } + } - if( ( int )ret.DefaultSettings >= ret.Count ) - ret.DefaultSettings = 0; - - return ret; + public bool MoveOption( int optionIdxFrom, int optionIdxTo ) + { + if( !OptionData.Move( optionIdxFrom, optionIdxTo ) ) + { + return false; } - public IModGroup Convert( GroupType type ) + // Update default settings with the move. + if( DefaultSettings == optionIdxFrom ) { - switch( type ) + DefaultSettings = ( uint )optionIdxTo; + } + else if( optionIdxFrom < optionIdxTo ) + { + if( DefaultSettings > optionIdxFrom && DefaultSettings <= optionIdxTo ) { - case GroupType.Single: return this; - case GroupType.Multi: - var multi = new MultiModGroup() - { - Name = Name, - Description = Description, - Priority = Priority, - DefaultSettings = 1u << ( int )DefaultSettings, - }; - multi.PrioritizedOptions.AddRange( OptionData.Select( ( o, i ) => ( o, i ) ) ); - return multi; - default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); + --DefaultSettings; } } - - public bool MoveOption( int optionIdxFrom, int optionIdxTo ) + else if( DefaultSettings < optionIdxFrom && DefaultSettings >= optionIdxTo ) { - if( !OptionData.Move( optionIdxFrom, optionIdxTo ) ) - { - return false; - } - - // Update default settings with the move. - if( DefaultSettings == optionIdxFrom ) - { - DefaultSettings = ( uint )optionIdxTo; - } - else if( optionIdxFrom < optionIdxTo ) - { - if( DefaultSettings > optionIdxFrom && DefaultSettings <= optionIdxTo ) - { - --DefaultSettings; - } - } - else if( DefaultSettings < optionIdxFrom && DefaultSettings >= optionIdxTo ) - { - ++DefaultSettings; - } - - UpdatePositions( Math.Min( optionIdxFrom, optionIdxTo ) ); - return true; + ++DefaultSettings; } - public void UpdatePositions( int from = 0 ) + UpdatePositions( Math.Min( optionIdxFrom, optionIdxTo ) ); + return true; + } + + public void UpdatePositions( int from = 0 ) + { + foreach( var (o, i) in OptionData.WithIndex().Skip( from ) ) { - foreach( var (o, i) in OptionData.WithIndex().Skip( from ) ) - { - o.SetPosition( o.GroupIdx, i ); - } + o.SetPosition( o.GroupIdx, i ); } } } \ No newline at end of file diff --git a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs index 9e609891..f1a4a24c 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.SubMod.cs @@ -36,7 +36,7 @@ public partial class Mod ISubMod.WriteSubMod( j, serializer, _default, ModPath, 0 ); } - private void SaveDefaultModDelayed() + internal void SaveDefaultModDelayed() => Penumbra.Framework.RegisterDelayed( nameof( SaveDefaultMod ) + ModPath.Name, SaveDefaultMod ); private void LoadDefaultOption() @@ -92,233 +92,237 @@ public partial class Mod } - // A sub mod is a collection of - // - file replacements - // - file swaps - // - meta manipulations - // that can be used either as an option or as the default data for a mod. - // It can be loaded and reloaded from Json. - // Nothing is checked for existence or validity when loading. - // Objects are also not checked for uniqueness, the first appearance of a game path or meta path decides. - public sealed class SubMod : ISubMod + +} + +/// +/// A sub mod is a collection of +/// - file replacements +/// - file swaps +/// - meta manipulations +/// that can be used either as an option or as the default data for a mod. +/// It can be loaded and reloaded from Json. +/// Nothing is checked for existence or validity when loading. +/// Objects are also not checked for uniqueness, the first appearance of a game path or meta path decides. +/// + public sealed class SubMod : ISubMod +{ + public string Name { get; set; } = "Default"; + + public string FullName + => GroupIdx < 0 ? "Default Option" : $"{ParentMod.Groups[ GroupIdx ].Name}: {Name}"; + + public string Description { get; set; } = string.Empty; + + internal IMod ParentMod { get; private init; } + internal int GroupIdx { get; private set; } + internal int OptionIdx { get; private set; } + + public bool IsDefault + => GroupIdx < 0; + + public Dictionary< Utf8GamePath, FullPath > FileData = new(); + public Dictionary< Utf8GamePath, FullPath > FileSwapData = new(); + public HashSet< MetaManipulation > ManipulationData = new(); + + public SubMod( IMod parentMod ) + => ParentMod = parentMod; + + public IReadOnlyDictionary< Utf8GamePath, FullPath > Files + => FileData; + + public IReadOnlyDictionary< Utf8GamePath, FullPath > FileSwaps + => FileSwapData; + + public IReadOnlySet< MetaManipulation > Manipulations + => ManipulationData; + + public void SetPosition( int groupIdx, int optionIdx ) { - public string Name { get; set; } = "Default"; + GroupIdx = groupIdx; + OptionIdx = optionIdx; + } - public string FullName - => GroupIdx < 0 ? "Default Option" : $"{ParentMod.Groups[ GroupIdx ].Name}: {Name}"; + public void Load( DirectoryInfo basePath, JToken json, out int priority ) + { + FileData.Clear(); + FileSwapData.Clear(); + ManipulationData.Clear(); - public string Description { get; set; } = string.Empty; + // Every option has a name, but priorities are only relevant for multi group options. + Name = json[ nameof( ISubMod.Name ) ]?.ToObject< string >() ?? string.Empty; + Description = json[ nameof( ISubMod.Description ) ]?.ToObject< string >() ?? string.Empty; + priority = json[ nameof( IModGroup.Priority ) ]?.ToObject< int >() ?? 0; - internal IMod ParentMod { get; private init; } - internal int GroupIdx { get; private set; } - internal int OptionIdx { get; private set; } - - public bool IsDefault - => GroupIdx < 0; - - public Dictionary< Utf8GamePath, FullPath > FileData = new(); - public Dictionary< Utf8GamePath, FullPath > FileSwapData = new(); - public HashSet< MetaManipulation > ManipulationData = new(); - - public SubMod( IMod parentMod ) - => ParentMod = parentMod; - - public IReadOnlyDictionary< Utf8GamePath, FullPath > Files - => FileData; - - public IReadOnlyDictionary< Utf8GamePath, FullPath > FileSwaps - => FileSwapData; - - public IReadOnlySet< MetaManipulation > Manipulations - => ManipulationData; - - public void SetPosition( int groupIdx, int optionIdx ) + var files = ( JObject? )json[ nameof( Files ) ]; + if( files != null ) { - GroupIdx = groupIdx; - OptionIdx = optionIdx; - } - - public void Load( DirectoryInfo basePath, JToken json, out int priority ) - { - FileData.Clear(); - FileSwapData.Clear(); - ManipulationData.Clear(); - - // Every option has a name, but priorities are only relevant for multi group options. - Name = json[ nameof( ISubMod.Name ) ]?.ToObject< string >() ?? string.Empty; - Description = json[ nameof( ISubMod.Description ) ]?.ToObject< string >() ?? string.Empty; - priority = json[ nameof( IModGroup.Priority ) ]?.ToObject< int >() ?? 0; - - var files = ( JObject? )json[ nameof( Files ) ]; - if( files != null ) + foreach( var property in files.Properties() ) { - foreach( var property in files.Properties() ) + if( Utf8GamePath.FromString( property.Name, out var p, true ) ) { - if( Utf8GamePath.FromString( property.Name, out var p, true ) ) - { - FileData.TryAdd( p, new FullPath( basePath, property.Value.ToObject< Utf8RelPath >() ) ); - } - } - } - - var swaps = ( JObject? )json[ nameof( FileSwaps ) ]; - if( swaps != null ) - { - foreach( var property in swaps.Properties() ) - { - if( Utf8GamePath.FromString( property.Name, out var p, true ) ) - { - FileSwapData.TryAdd( p, new FullPath( property.Value.ToObject< string >()! ) ); - } - } - } - - var manips = json[ nameof( Manipulations ) ]; - if( manips != null ) - { - foreach( var s in manips.Children().Select( c => c.ToObject< MetaManipulation >() ).Where( m => m.ManipulationType != MetaManipulation.Type.Unknown ) ) - { - ManipulationData.Add( s ); + FileData.TryAdd( p, new FullPath( basePath, property.Value.ToObject< Utf8RelPath >() ) ); } } } - // If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod. - // If delete is true, the files are deleted afterwards. - public (bool Changes, List< string > DeleteList) IncorporateMetaChanges( DirectoryInfo basePath, bool delete ) + var swaps = ( JObject? )json[ nameof( FileSwaps ) ]; + if( swaps != null ) { - var deleteList = new List< string >(); - var oldSize = ManipulationData.Count; - var deleteString = delete ? "with deletion." : "without deletion."; - foreach( var (key, file) in Files.ToList() ) + foreach( var property in swaps.Properties() ) { - var ext1 = key.Extension().AsciiToLower().ToString(); - var ext2 = file.Extension.ToLowerInvariant(); - try + if( Utf8GamePath.FromString( property.Name, out var p, true ) ) { - if( ext1 == ".meta" || ext2 == ".meta" ) - { - FileData.Remove( key ); - if( !file.Exists ) - { - continue; - } - - var meta = new TexToolsMeta( Penumbra.GamePathParser, File.ReadAllBytes( file.FullName ), Penumbra.Config.KeepDefaultMetaChanges ); - Penumbra.Log.Verbose( $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}" ); - deleteList.Add( file.FullName ); - ManipulationData.UnionWith( meta.MetaManipulations ); - } - else if( ext1 == ".rgsp" || ext2 == ".rgsp" ) - { - FileData.Remove( key ); - if( !file.Exists ) - { - continue; - } - - var rgsp = TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ), Penumbra.Config.KeepDefaultMetaChanges ); - Penumbra.Log.Verbose( $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}" ); - deleteList.Add( file.FullName ); - - ManipulationData.UnionWith( rgsp.MetaManipulations ); - } - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not incorporate meta changes in mod {basePath} from file {file.FullName}:\n{e}" ); - } - } - - DeleteDeleteList( deleteList, delete ); - return ( oldSize < ManipulationData.Count, deleteList ); - } - - internal static void DeleteDeleteList( IEnumerable< string > deleteList, bool delete ) - { - if( !delete ) - { - return; - } - - foreach( var file in deleteList ) - { - try - { - File.Delete( file ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not delete incorporated meta file {file}:\n{e}" ); + FileSwapData.TryAdd( p, new FullPath( property.Value.ToObject< string >()! ) ); } } } - public void WriteTexToolsMeta( DirectoryInfo basePath, bool test = false ) + var manips = json[ nameof( Manipulations ) ]; + if( manips != null ) { - var files = TexToolsMeta.ConvertToTexTools( Manipulations ); - - foreach( var (file, data) in files ) + foreach( var s in manips.Children().Select( c => c.ToObject< MetaManipulation >() ).Where( m => m.ManipulationType != MetaManipulation.Type.Unknown ) ) { - var path = Path.Combine( basePath.FullName, file ); - try - { - Directory.CreateDirectory( Path.GetDirectoryName( path )! ); - File.WriteAllBytes( path, data ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not write meta file {path}:\n{e}" ); - } - } - - if( test ) - { - TestMetaWriting( files ); - } - } - - [Conditional( "DEBUG" )] - private void TestMetaWriting( Dictionary< string, byte[] > files ) - { - var meta = new HashSet< MetaManipulation >( Manipulations.Count ); - foreach( var (file, data) in files ) - { - try - { - var x = file.EndsWith( "rgsp" ) - ? TexToolsMeta.FromRgspFile( file, data, Penumbra.Config.KeepDefaultMetaChanges ) - : new TexToolsMeta( Penumbra.GamePathParser, data, Penumbra.Config.KeepDefaultMetaChanges ); - meta.UnionWith( x.MetaManipulations ); - } - catch - { - // ignored - } - } - - if( !Manipulations.SetEquals( meta ) ) - { - Penumbra.Log.Information( "Meta Sets do not equal." ); - foreach( var (m1, m2) in Manipulations.Zip( meta ) ) - { - Penumbra.Log.Information( $"{m1} {m1.EntryToString()} | {m2} {m2.EntryToString()}" ); - } - - foreach( var m in Manipulations.Skip( meta.Count ) ) - { - Penumbra.Log.Information( $"{m} {m.EntryToString()} " ); - } - - foreach( var m in meta.Skip( Manipulations.Count ) ) - { - Penumbra.Log.Information( $"{m} {m.EntryToString()} " ); - } - } - else - { - Penumbra.Log.Information( "Meta Sets are equal." ); + ManipulationData.Add( s ); } } } + + // If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod. + // If delete is true, the files are deleted afterwards. + public (bool Changes, List< string > DeleteList) IncorporateMetaChanges( DirectoryInfo basePath, bool delete ) + { + var deleteList = new List< string >(); + var oldSize = ManipulationData.Count; + var deleteString = delete ? "with deletion." : "without deletion."; + foreach( var (key, file) in Files.ToList() ) + { + var ext1 = key.Extension().AsciiToLower().ToString(); + var ext2 = file.Extension.ToLowerInvariant(); + try + { + if( ext1 == ".meta" || ext2 == ".meta" ) + { + FileData.Remove( key ); + if( !file.Exists ) + { + continue; + } + + var meta = new TexToolsMeta( Penumbra.GamePathParser, File.ReadAllBytes( file.FullName ), Penumbra.Config.KeepDefaultMetaChanges ); + Penumbra.Log.Verbose( $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}" ); + deleteList.Add( file.FullName ); + ManipulationData.UnionWith( meta.MetaManipulations ); + } + else if( ext1 == ".rgsp" || ext2 == ".rgsp" ) + { + FileData.Remove( key ); + if( !file.Exists ) + { + continue; + } + + var rgsp = TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ), Penumbra.Config.KeepDefaultMetaChanges ); + Penumbra.Log.Verbose( $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}" ); + deleteList.Add( file.FullName ); + + ManipulationData.UnionWith( rgsp.MetaManipulations ); + } + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not incorporate meta changes in mod {basePath} from file {file.FullName}:\n{e}" ); + } + } + + DeleteDeleteList( deleteList, delete ); + return ( oldSize < ManipulationData.Count, deleteList ); + } + + internal static void DeleteDeleteList( IEnumerable< string > deleteList, bool delete ) + { + if( !delete ) + { + return; + } + + foreach( var file in deleteList ) + { + try + { + File.Delete( file ); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not delete incorporated meta file {file}:\n{e}" ); + } + } + } + + public void WriteTexToolsMeta( DirectoryInfo basePath, bool test = false ) + { + var files = TexToolsMeta.ConvertToTexTools( Manipulations ); + + foreach( var (file, data) in files ) + { + var path = Path.Combine( basePath.FullName, file ); + try + { + Directory.CreateDirectory( Path.GetDirectoryName( path )! ); + File.WriteAllBytes( path, data ); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not write meta file {path}:\n{e}" ); + } + } + + if( test ) + { + TestMetaWriting( files ); + } + } + + [Conditional("DEBUG" )] + private void TestMetaWriting( Dictionary< string, byte[] > files ) + { + var meta = new HashSet< MetaManipulation >( Manipulations.Count ); + foreach( var (file, data) in files ) + { + try + { + var x = file.EndsWith( "rgsp" ) + ? TexToolsMeta.FromRgspFile( file, data, Penumbra.Config.KeepDefaultMetaChanges ) + : new TexToolsMeta( Penumbra.GamePathParser, data, Penumbra.Config.KeepDefaultMetaChanges ); + meta.UnionWith( x.MetaManipulations ); + } + catch + { + // ignored + } + } + + if( !Manipulations.SetEquals( meta ) ) + { + Penumbra.Log.Information( "Meta Sets do not equal." ); + foreach( var (m1, m2) in Manipulations.Zip( meta ) ) + { + Penumbra.Log.Information( $"{m1} {m1.EntryToString()} | {m2} {m2.EntryToString()}" ); + } + + foreach( var m in Manipulations.Skip( meta.Count ) ) + { + Penumbra.Log.Information( $"{m} {m.EntryToString()} " ); + } + + foreach( var m in meta.Skip( Manipulations.Count ) ) + { + Penumbra.Log.Information( $"{m} {m.EntryToString()} " ); + } + } + else + { + Penumbra.Log.Information( "Meta Sets are equal." ); + } + } } \ No newline at end of file diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs index 845456ae..a9239078 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Subclasses/ModSettings.cs @@ -10,7 +10,7 @@ using Penumbra.String.Classes; namespace Penumbra.Mods; -// Contains the settings for a given mod. +/// Contains the settings for a given mod. public class ModSettings { public static readonly ModSettings Empty = new(); diff --git a/Penumbra/Mods/TemporaryMod.cs b/Penumbra/Mods/TemporaryMod.cs index 5bf37c95..d46d00d5 100644 --- a/Penumbra/Mods/TemporaryMod.cs +++ b/Penumbra/Mods/TemporaryMod.cs @@ -27,10 +27,10 @@ public class TemporaryMod : IMod public IEnumerable< ISubMod > AllSubMods => new[] { Default }; - private readonly Mod.SubMod _default; + private readonly SubMod _default; public TemporaryMod() - => _default = new Mod.SubMod( this ); + => _default = new SubMod( this ); public void SetFile( Utf8GamePath gamePath, FullPath fullPath ) => _default.FileData[ gamePath ] = fullPath; @@ -44,7 +44,7 @@ public class TemporaryMod : IMod _default.ManipulationData = manips; } - public static void SaveTempCollection( Mod.Manager modManager, ModCollection collection, string? character = null ) + public static void SaveTempCollection( ModManager modManager, ModCollection collection, string? character = null ) { DirectoryInfo? dir = null; try @@ -54,7 +54,7 @@ public class TemporaryMod : IMod modManager.DataEditor.CreateMeta( dir, collection.Name, character ?? Penumbra.Config.DefaultModAuthor, $"Mod generated from temporary collection {collection.Name} for {character ?? "Unknown Character"}.", null, null ); var mod = new Mod( dir ); - var defaultMod = (Mod.SubMod) mod.Default; + var defaultMod = (SubMod) mod.Default; foreach( var (gamePath, fullPath) in collection.ResolvedFiles ) { if( gamePath.Path.EndsWith( ".imc"u8 ) ) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 46b8ad3a..85a7c717 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -47,8 +47,8 @@ public class Penumbra : IDalamudPlugin public static CharacterUtility CharacterUtility { get; private set; } = null!; public static GameEventManager GameEvents { get; private set; } = null!; public static MetaFileManager MetaFileManager { get; private set; } = null!; - public static Mod.Manager ModManager { get; private set; } = null!; - public static ModCollection.Manager CollectionManager { get; private set; } = null!; + public static ModManager ModManager { get; private set; } = null!; + public static CollectionManager CollectionManager { get; private set; } = null!; public static TempCollectionManager TempCollections { get; private set; } = null!; public static TempModManager TempMods { get; private set; } = null!; public static ResourceLoader ResourceLoader { get; private set; } = null!; @@ -96,8 +96,8 @@ public class Penumbra : IDalamudPlugin TempMods = _tmp.Services.GetRequiredService(); ResidentResources = _tmp.Services.GetRequiredService(); _tmp.Services.GetRequiredService(); - ModManager = _tmp.Services.GetRequiredService(); - CollectionManager = _tmp.Services.GetRequiredService(); + ModManager = _tmp.Services.GetRequiredService(); + CollectionManager = _tmp.Services.GetRequiredService(); TempCollections = _tmp.Services.GetRequiredService(); ModFileSystem = _tmp.Services.GetRequiredService(); RedrawService = _tmp.Services.GetRequiredService(); diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index 30ea8f73..fcf2c386 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -88,12 +88,12 @@ public class PenumbraNew // Add Collection Services services.AddTransient() .AddSingleton() - .AddSingleton(); + .AddSingleton(); // Add Mod Services services.AddSingleton() .AddSingleton() - .AddSingleton() + .AddSingleton() .AddSingleton(); // Add Resource services diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index b187e991..a2f8b55f 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -87,7 +87,7 @@ public class ConfigMigrationService if (_config.Version != 6) return; - ModCollection.Manager.MigrateUngenderedCollections(_fileNames); + CollectionManager.MigrateUngenderedCollections(_fileNames); _config.Version = 7; } @@ -113,7 +113,7 @@ public class ConfigMigrationService if (_config.Version != 4) return; - Mod.Manager.MigrateModBackups = true; + ModManager.MigrateModBackups = true; _config.Version = 5; } @@ -257,11 +257,11 @@ public class ConfigMigrationService using var j = new JsonTextWriter(writer); j.Formatting = Formatting.Indented; j.WriteStartObject(); - j.WritePropertyName(nameof(ModCollection.Manager.Default)); + j.WritePropertyName(nameof(CollectionManager.Default)); j.WriteValue(def); - j.WritePropertyName(nameof(ModCollection.Manager.Interface)); + j.WritePropertyName(nameof(CollectionManager.Interface)); j.WriteValue(ui); - j.WritePropertyName(nameof(ModCollection.Manager.Current)); + j.WritePropertyName(nameof(CollectionManager.Current)); j.WriteValue(current); foreach (var (type, collection) in special) { diff --git a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs index 3a5d8ce6..48333d6f 100644 --- a/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs +++ b/Penumbra/UI/AdvancedWindow/ItemSwapTab.cs @@ -25,12 +25,12 @@ public class ItemSwapTab : IDisposable, ITab { private readonly CommunicatorService _communicator; private readonly ItemService _itemService; - private readonly ModCollection.Manager _collectionManager; - private readonly Mod.Manager _modManager; + private readonly CollectionManager _collectionManager; + private readonly ModManager _modManager; private readonly Configuration _config; - public ItemSwapTab(CommunicatorService communicator, ItemService itemService, ModCollection.Manager collectionManager, - Mod.Manager modManager, Configuration config) + public ItemSwapTab(CommunicatorService communicator, ItemService itemService, CollectionManager collectionManager, + ModManager modManager, Configuration config) { _communicator = communicator; _itemService = itemService; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index 55eecdae..ad0e7fa2 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -297,7 +297,7 @@ public partial class ModEditWindow var tt = changes ? "Apply the current file setup to the currently selected option." : "No changes made."; if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, !changes)) { - var failedFiles = _editor.FileEditor.Apply(_editor.Mod!, (Mod.SubMod)_editor.Option!); + var failedFiles = _editor.FileEditor.Apply(_editor.Mod!, (SubMod)_editor.Option!); if (failedFiles > 0) Penumbra.Log.Information($"Failed to apply {failedFiles} file redirections to {_editor.Option!.FullName}."); } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 29671c7c..fae1f0a0 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -194,7 +194,7 @@ public partial class ModEditWindow _editor.FileEditor.Revert(_editor.Mod!, _editor.Option!); var fileRegistry = _editor.Files.Available.First(file => file.File.FullName == _targetPath); _editor.FileEditor.AddPathsToSelected(_editor.Option!, new []{ fileRegistry }, _subDirs); - _editor.FileEditor.Apply(_editor.Mod!, (Mod.SubMod) _editor.Option!); + _editor.FileEditor.Apply(_editor.Mod!, (SubMod) _editor.Option!); return fileRegistry; } diff --git a/Penumbra/UI/CollectionTab/Collections.CollectionSelector.cs b/Penumbra/UI/CollectionTab/Collections.CollectionSelector.cs index bd9d3e35..1ff60559 100644 --- a/Penumbra/UI/CollectionTab/Collections.CollectionSelector.cs +++ b/Penumbra/UI/CollectionTab/Collections.CollectionSelector.cs @@ -9,9 +9,9 @@ namespace Penumbra.UI.CollectionTab; public sealed class CollectionSelector : FilterComboCache { - private readonly ModCollection.Manager _collectionManager; + private readonly CollectionManager _collectionManager; - public CollectionSelector(ModCollection.Manager manager, Func> items) + public CollectionSelector(CollectionManager manager, Func> items) : base(items) => _collectionManager = manager; diff --git a/Penumbra/UI/CollectionTab/Collections.IndividualCollectionUi.cs b/Penumbra/UI/CollectionTab/Collections.IndividualCollectionUi.cs index a29ebb25..b7bcc0f5 100644 --- a/Penumbra/UI/CollectionTab/Collections.IndividualCollectionUi.cs +++ b/Penumbra/UI/CollectionTab/Collections.IndividualCollectionUi.cs @@ -16,10 +16,10 @@ namespace Penumbra.UI.CollectionTab; public class IndividualCollectionUi { private readonly ActorService _actorService; - private readonly ModCollection.Manager _collectionManager; + private readonly CollectionManager _collectionManager; private readonly CollectionSelector _withEmpty; - public IndividualCollectionUi(ActorService actors, ModCollection.Manager collectionManager, CollectionSelector withEmpty) + public IndividualCollectionUi(ActorService actors, CollectionManager collectionManager, CollectionSelector withEmpty) { _actorService = actors; _collectionManager = collectionManager; diff --git a/Penumbra/UI/CollectionTab/Collections.InheritanceUi.cs b/Penumbra/UI/CollectionTab/Collections.InheritanceUi.cs index dc3bc52f..57a51ab1 100644 --- a/Penumbra/UI/CollectionTab/Collections.InheritanceUi.cs +++ b/Penumbra/UI/CollectionTab/Collections.InheritanceUi.cs @@ -16,9 +16,9 @@ public class InheritanceUi private const int InheritedCollectionHeight = 9; private const string InheritanceDragDropLabel = "##InheritanceMove"; - private readonly ModCollection.Manager _collectionManager; + private readonly CollectionManager _collectionManager; - public InheritanceUi(ModCollection.Manager collectionManager) + public InheritanceUi(CollectionManager collectionManager) => _collectionManager = collectionManager; /// Draw the whole inheritance block. diff --git a/Penumbra/UI/CollectionTab/Collections.SpecialCombo.cs b/Penumbra/UI/CollectionTab/Collections.SpecialCombo.cs index 59461f42..5af7c578 100644 --- a/Penumbra/UI/CollectionTab/Collections.SpecialCombo.cs +++ b/Penumbra/UI/CollectionTab/Collections.SpecialCombo.cs @@ -7,7 +7,7 @@ namespace Penumbra.UI.CollectionTab; public sealed class SpecialCombo : FilterComboBase<(CollectionType, string, string)> { - private readonly ModCollection.Manager _collectionManager; + private readonly CollectionManager _collectionManager; public (CollectionType, string, string)? CurrentType => CollectionTypeExtensions.Special[CurrentIdx]; @@ -16,7 +16,7 @@ public sealed class SpecialCombo : FilterComboBase<(CollectionType, string, stri private readonly float _unscaledWidth; private readonly string _label; - public SpecialCombo(ModCollection.Manager collectionManager, string label, float unscaledWidth) + public SpecialCombo(CollectionManager collectionManager, string label, float unscaledWidth) : base(CollectionTypeExtensions.Special, false) { _collectionManager = collectionManager; diff --git a/Penumbra/UI/FileDialogService.cs b/Penumbra/UI/FileDialogService.cs index 2d16668f..650014c8 100644 --- a/Penumbra/UI/FileDialogService.cs +++ b/Penumbra/UI/FileDialogService.cs @@ -14,12 +14,12 @@ namespace Penumbra.UI; public class FileDialogService : IDisposable { - private readonly Mod.Manager _mods; + private readonly ModManager _mods; private readonly FileDialogManager _manager; private readonly ConcurrentDictionary _startPaths = new(); private bool _isOpen; - public FileDialogService(Mod.Manager mods, Configuration config) + public FileDialogService(ModManager mods, Configuration config) { _mods = mods; _manager = SetupFileManager(config.ModDirectory); diff --git a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs index 64febc57..cbc834ac 100644 --- a/Penumbra/UI/ModsTab/ModFileSystemSelector.cs +++ b/Penumbra/UI/ModsTab/ModFileSystemSelector.cs @@ -29,8 +29,8 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector _newGroupName = string.Empty; - public static void Draw(Mod.Manager modManager, Mod mod) + public static void Draw(ModManager modManager, Mod mod) { using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3)); ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3); @@ -250,15 +250,15 @@ public class ModPanelEditTab : ITab private static class MoveDirectory { private static string? _currentModDirectory; - private static Mod.Manager.NewDirectoryState _state = Mod.Manager.NewDirectoryState.Identical; + private static ModManager.NewDirectoryState _state = ModManager.NewDirectoryState.Identical; public static void Reset() { _currentModDirectory = null; - _state = Mod.Manager.NewDirectoryState.Identical; + _state = ModManager.NewDirectoryState.Identical; } - public static void Draw(Mod.Manager modManager, Mod mod, Vector2 buttonSize) + public static void Draw(ModManager modManager, Mod mod, Vector2 buttonSize) { ImGui.SetNextItemWidth(buttonSize.X * 2 + ImGui.GetStyle().ItemSpacing.X); var tmp = _currentModDirectory ?? mod.ModPath.Name; @@ -270,13 +270,13 @@ public class ModPanelEditTab : ITab var (disabled, tt) = _state switch { - Mod.Manager.NewDirectoryState.Identical => (true, "Current directory name is identical to new one."), - Mod.Manager.NewDirectoryState.Empty => (true, "Please enter a new directory name first."), - Mod.Manager.NewDirectoryState.NonExisting => (false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}."), - Mod.Manager.NewDirectoryState.ExistsEmpty => (false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}."), - Mod.Manager.NewDirectoryState.ExistsNonEmpty => (true, $"{_currentModDirectory} already exists and is not empty."), - Mod.Manager.NewDirectoryState.ExistsAsFile => (true, $"{_currentModDirectory} exists as a file."), - Mod.Manager.NewDirectoryState.ContainsInvalidSymbols => (true, + ModManager.NewDirectoryState.Identical => (true, "Current directory name is identical to new one."), + ModManager.NewDirectoryState.Empty => (true, "Please enter a new directory name first."), + ModManager.NewDirectoryState.NonExisting => (false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}."), + ModManager.NewDirectoryState.ExistsEmpty => (false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}."), + ModManager.NewDirectoryState.ExistsNonEmpty => (true, $"{_currentModDirectory} already exists and is not empty."), + ModManager.NewDirectoryState.ExistsAsFile => (true, $"{_currentModDirectory} exists as a file."), + ModManager.NewDirectoryState.ContainsInvalidSymbols => (true, $"{_currentModDirectory} contains invalid symbols for FFXIV."), _ => (true, "Unknown error."), }; @@ -317,7 +317,7 @@ public class ModPanelEditTab : ITab ImGui.OpenPopup(PopupName); } - public static void DrawPopup(Mod.Manager modManager) + public static void DrawPopup(ModManager modManager) { if (_mod == null) return; diff --git a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs index b0936d90..72dbe9fa 100644 --- a/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs +++ b/Penumbra/UI/ModsTab/ModPanelSettingsTab.cs @@ -18,11 +18,11 @@ namespace Penumbra.UI.ModsTab; public class ModPanelSettingsTab : ITab { private readonly Configuration _config; - private readonly ModCollection.Manager _collectionManager; + private readonly CollectionManager _collectionManager; private readonly ModFileSystemSelector _selector; private readonly TutorialService _tutorial; private readonly PenumbraApi _api; - private readonly Mod.Manager _modManager; + private readonly ModManager _modManager; private bool _inherited; private ModSettings _settings = null!; @@ -30,7 +30,7 @@ public class ModPanelSettingsTab : ITab private bool _empty; private int? _currentPriority = null; - public ModPanelSettingsTab(ModCollection.Manager collectionManager, Mod.Manager modManager, ModFileSystemSelector selector, + public ModPanelSettingsTab(CollectionManager collectionManager, ModManager modManager, ModFileSystemSelector selector, TutorialService tutorial, PenumbraApi api, Configuration config) { _collectionManager = collectionManager; diff --git a/Penumbra/UI/ModsTab/ModPanelTabBar.cs b/Penumbra/UI/ModsTab/ModPanelTabBar.cs index d0006b17..77f2b1f2 100644 --- a/Penumbra/UI/ModsTab/ModPanelTabBar.cs +++ b/Penumbra/UI/ModsTab/ModPanelTabBar.cs @@ -27,7 +27,7 @@ public class ModPanelTabBar public readonly ModPanelChangedItemsTab ChangedItems; public readonly ModPanelEditTab Edit; private readonly ModEditWindow _modEditWindow; - private readonly Mod.Manager _modManager; + private readonly ModManager _modManager; private readonly TutorialService _tutorial; public readonly ITab[] Tabs; @@ -35,7 +35,7 @@ public class ModPanelTabBar private Mod? _lastMod = null; public ModPanelTabBar(ModEditWindow modEditWindow, ModPanelSettingsTab settings, ModPanelDescriptionTab description, - ModPanelConflictsTab conflicts, ModPanelChangedItemsTab changedItems, ModPanelEditTab edit, Mod.Manager modManager, + ModPanelConflictsTab conflicts, ModPanelChangedItemsTab changedItems, ModPanelEditTab edit, ModManager modManager, TutorialService tutorial) { _modEditWindow = modEditWindow; @@ -107,7 +107,7 @@ public class ModPanelTabBar if (ImGui.TabItemButton("Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip)) { _modEditWindow.ChangeMod(mod); - _modEditWindow.ChangeOption((Mod.SubMod) mod.Default); + _modEditWindow.ChangeOption((SubMod) mod.Default); _modEditWindow.IsOpen = true; } diff --git a/Penumbra/UI/Tabs/ChangedItemsTab.cs b/Penumbra/UI/Tabs/ChangedItemsTab.cs index 22eecf7e..e5f9083e 100644 --- a/Penumbra/UI/Tabs/ChangedItemsTab.cs +++ b/Penumbra/UI/Tabs/ChangedItemsTab.cs @@ -16,10 +16,10 @@ namespace Penumbra.UI.Tabs; public class ChangedItemsTab : ITab { - private readonly ModCollection.Manager _collectionManager; + private readonly CollectionManager _collectionManager; private readonly PenumbraApi _api; - public ChangedItemsTab(ModCollection.Manager collectionManager, PenumbraApi api) + public ChangedItemsTab(CollectionManager collectionManager, PenumbraApi api) { _collectionManager = collectionManager; _api = api; diff --git a/Penumbra/UI/Tabs/CollectionsTab.cs b/Penumbra/UI/Tabs/CollectionsTab.cs index 4825b4aa..afd739a8 100644 --- a/Penumbra/UI/Tabs/CollectionsTab.cs +++ b/Penumbra/UI/Tabs/CollectionsTab.cs @@ -17,7 +17,7 @@ public class CollectionsTab : IDisposable, ITab { private readonly CommunicatorService _communicator; private readonly Configuration _config; - private readonly ModCollection.Manager _collectionManager; + private readonly CollectionManager _collectionManager; private readonly TutorialService _tutorial; private readonly SpecialCombo _specialCollectionCombo; @@ -26,7 +26,7 @@ public class CollectionsTab : IDisposable, ITab private readonly InheritanceUi _inheritance; private readonly IndividualCollectionUi _individualCollections; - public CollectionsTab(ActorService actorService, CommunicatorService communicator, ModCollection.Manager collectionManager, + public CollectionsTab(ActorService actorService, CommunicatorService communicator, CollectionManager collectionManager, TutorialService tutorial, Configuration config) { _communicator = communicator; diff --git a/Penumbra/UI/Tabs/DebugTab.cs b/Penumbra/UI/Tabs/DebugTab.cs index 2c89e673..09dbc097 100644 --- a/Penumbra/UI/Tabs/DebugTab.cs +++ b/Penumbra/UI/Tabs/DebugTab.cs @@ -34,8 +34,8 @@ public class DebugTab : ITab private readonly StartTracker _timer; private readonly PerformanceTracker _performance; private readonly Configuration _config; - private readonly ModCollection.Manager _collectionManager; - private readonly Mod.Manager _modManager; + private readonly CollectionManager _collectionManager; + private readonly ModManager _modManager; private readonly ValidityChecker _validityChecker; private readonly HttpApi _httpApi; private readonly ActorService _actorService; @@ -52,8 +52,8 @@ public class DebugTab : ITab private readonly IdentifiedCollectionCache _identifiedCollectionCache; private readonly CutsceneService _cutsceneService; - public DebugTab(StartTracker timer, PerformanceTracker performance, Configuration config, ModCollection.Manager collectionManager, - ValidityChecker validityChecker, Mod.Manager modManager, HttpApi httpApi, ActorService actorService, + public DebugTab(StartTracker timer, PerformanceTracker performance, Configuration config, CollectionManager collectionManager, + ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorService actorService, DalamudServices dalamud, StainService stains, CharacterUtility characterUtility, ResidentResourceManager residentResources, ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver, DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache, diff --git a/Penumbra/UI/Tabs/EffectiveTab.cs b/Penumbra/UI/Tabs/EffectiveTab.cs index 45e244d8..5f455189 100644 --- a/Penumbra/UI/Tabs/EffectiveTab.cs +++ b/Penumbra/UI/Tabs/EffectiveTab.cs @@ -17,9 +17,9 @@ namespace Penumbra.UI.Tabs; public class EffectiveTab : ITab { - private readonly ModCollection.Manager _collectionManager; + private readonly CollectionManager _collectionManager; - public EffectiveTab(ModCollection.Manager collectionManager) + public EffectiveTab(CollectionManager collectionManager) => _collectionManager = collectionManager; public ReadOnlySpan Label diff --git a/Penumbra/UI/Tabs/ModsTab.cs b/Penumbra/UI/Tabs/ModsTab.cs index f6e0f2ed..5c1340a5 100644 --- a/Penumbra/UI/Tabs/ModsTab.cs +++ b/Penumbra/UI/Tabs/ModsTab.cs @@ -23,13 +23,13 @@ public class ModsTab : ITab private readonly ModFileSystemSelector _selector; private readonly ModPanel _panel; private readonly TutorialService _tutorial; - private readonly Mod.Manager _modManager; - private readonly ModCollection.Manager _collectionManager; + private readonly ModManager _modManager; + private readonly CollectionManager _collectionManager; private readonly RedrawService _redrawService; private readonly Configuration _config; private readonly CollectionsTab _collectionsTab; - public ModsTab(Mod.Manager modManager, ModCollection.Manager collectionManager, ModFileSystemSelector selector, ModPanel panel, + public ModsTab(ModManager modManager, CollectionManager collectionManager, ModFileSystemSelector selector, ModPanel panel, TutorialService tutorial, RedrawService redrawService, Configuration config, CollectionsTab collectionsTab) { _modManager = modManager; diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index a0dd3c9f..039a648f 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -31,14 +31,14 @@ public class SettingsTab : ITab private readonly TutorialService _tutorial; private readonly Penumbra _penumbra; private readonly FileDialogService _fileDialog; - private readonly Mod.Manager _modManager; + private readonly ModManager _modManager; private readonly ModFileSystemSelector _selector; private readonly CharacterUtility _characterUtility; private readonly ResidentResourceManager _residentResources; private readonly DalamudServices _dalamud; public SettingsTab(Configuration config, FontReloader fontReloader, TutorialService tutorial, Penumbra penumbra, - FileDialogService fileDialog, Mod.Manager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility, + FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility, ResidentResourceManager residentResources, DalamudServices dalamud) { _config = config;