diff --git a/Penumbra/Collections/Manager/CollectionCacheManager.cs b/Penumbra/Collections/Cache/CollectionCacheManager.cs similarity index 96% rename from Penumbra/Collections/Manager/CollectionCacheManager.cs rename to Penumbra/Collections/Cache/CollectionCacheManager.cs index 56a6e3a3..d2604418 100644 --- a/Penumbra/Collections/Manager/CollectionCacheManager.cs +++ b/Penumbra/Collections/Cache/CollectionCacheManager.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Threading.Tasks; using Penumbra.Api; using Penumbra.Api.Enums; +using Penumbra.Collections.Cache; using Penumbra.Interop.Services; using Penumbra.Mods; using Penumbra.Mods.Manager; @@ -20,6 +21,7 @@ public class CollectionCacheManager : IDisposable, IReadOnlyDictionary private readonly Dictionary _cache = new(); public int Count diff --git a/Penumbra/Collections/Cache/ModCollectionCache.cs b/Penumbra/Collections/Cache/ModCollectionCache.cs new file mode 100644 index 00000000..32cc9a8e --- /dev/null +++ b/Penumbra/Collections/Cache/ModCollectionCache.cs @@ -0,0 +1,485 @@ +using OtterGui; +using OtterGui.Classes; +using Penumbra.Meta.Manager; +using Penumbra.Meta.Manipulations; +using Penumbra.Mods; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Penumbra.Api.Enums; +using Penumbra.String.Classes; +using Penumbra.Mods.Manager; + +namespace Penumbra.Collections.Cache; + +public record struct ModPath(IMod Mod, FullPath Path); +public record ModConflicts(IMod Mod2, List Conflicts, bool HasPriority, bool Solved); + +/// +/// The Cache contains all required temporary data to use a collection. +/// It will only be setup if a collection gets activated in any way. +/// +public class ModCollectionCache : IDisposable +{ + private readonly ModCollection _collection; + private readonly SortedList, object?)> _changedItems = new(); + public readonly Dictionary ResolvedFiles = new(); + public readonly MetaManager MetaManipulations; + private readonly Dictionary> _conflicts = new(); + + public IEnumerable> AllConflicts + => _conflicts.Values; + + public SingleArray Conflicts(IMod mod) + => _conflicts.TryGetValue(mod, out var c) ? c : new SingleArray(); + + private int _changedItemsSaveCounter = -1; + + // Obtain currently changed items. Computes them if they haven't been computed before. + public IReadOnlyDictionary, object?)> ChangedItems + { + get + { + SetChangedItems(); + return _changedItems; + } + } + + // The cache reacts through events on its collection changing. + public ModCollectionCache(ModCollection collection) + { + _collection = collection; + MetaManipulations = new MetaManager(_collection); + } + + public void Dispose() + { + MetaManipulations.Dispose(); + } + + // 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 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[] ReverseResolvePaths(IReadOnlyCollection fullPaths) + { + if (fullPaths.Count == 0) + return Array.Empty>(); + + var ret = new HashSet[fullPaths.Count]; + var dict = new Dictionary(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 { utf8 } + : new HashSet(); + } + + foreach (var (game, full) in ResolvedFiles) + { + if (dict.TryGetValue(full.Path, out var idx)) + { + ret[idx].Add(game); + } + } + + return ret; + } + + 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())) + { + 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); + } + } + } + + if (addMetaChanges) + { + ++_collection.ChangeCounter; + if (_collection == Penumbra.CollectionManager.Active.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))) + { + AddSubMod(option, mod); + } + + break; + } + } + } + } + + AddSubMod(mod.Default, mod); + + if (addMetaChanges) + { + ++_collection.ChangeCounter; + if (Penumbra.ModCaches[mod.Index].TotalManipulations > 0) + { + AddMetaFiles(); + } + + if (_collection == Penumbra.CollectionManager.Active.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); + } + + 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 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 { 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(512); + + void AddItems(IMod mod) + { + foreach (var (name, obj) in items) + { + if (!_changedItems.TryGetValue(name, out var data)) + { + _changedItems.Add(name, (new SingleArray(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) + { + ModCacheManager.ComputeChangedItems(identifier, 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/Manager/CollectionManager.cs b/Penumbra/Collections/Manager/CollectionManager.cs index 45e10c6a..5e1c5781 100644 --- a/Penumbra/Collections/Manager/CollectionManager.cs +++ b/Penumbra/Collections/Manager/CollectionManager.cs @@ -19,6 +19,4 @@ public class CollectionManager Temp = temp; Editor = editor; } - - } diff --git a/Penumbra/Collections/Manager/IndividualCollections.Files.cs b/Penumbra/Collections/Manager/IndividualCollections.Files.cs index bc0d5737..8adf03fa 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.Files.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.Files.cs @@ -4,7 +4,6 @@ using System.Linq; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Interface.Internal.Notifications; using Newtonsoft.Json.Linq; -using Penumbra.Collections.Manager; using Penumbra.GameData.Actors; using Penumbra.String; diff --git a/Penumbra/Collections/ModCollection.Migration.cs b/Penumbra/Collections/Manager/ModCollectionMigration.cs similarity index 97% rename from Penumbra/Collections/ModCollection.Migration.cs rename to Penumbra/Collections/Manager/ModCollectionMigration.cs index 02ecea47..c0ddd0e4 100644 --- a/Penumbra/Collections/ModCollection.Migration.cs +++ b/Penumbra/Collections/Manager/ModCollectionMigration.cs @@ -4,20 +4,20 @@ using System.Linq; using Penumbra.Mods.Manager; using Penumbra.Util; -namespace Penumbra.Collections; +namespace Penumbra.Collections.Manager; /// Migration to convert ModCollections from older versions to newer. internal static class ModCollectionMigration { - /// Migrate a mod collection to the current version. + /// Migrate a mod collection to the current version. public static void Migrate(SaveService saver, ModStorage mods, int version, ModCollection collection) - { + { var changes = MigrateV0ToV1(collection, ref version); if (changes) saver.ImmediateSave(new ModCollectionSave(mods, collection)); } - - /// Migrate a mod collection from Version 0 to Version 1, which introduced support for inheritance. + + /// Migrate a mod collection from Version 0 to Version 1, which introduced support for inheritance. private static bool MigrateV0ToV1(ModCollection collection, ref int version) { if (version > 0) @@ -38,10 +38,10 @@ internal static class ModCollectionMigration return true; } - /// We treat every completely defaulted setting as inheritance-ready. + /// We treat every completely defaulted setting as inheritance-ready. private static bool SettingIsDefaultV0(ModSettings.SavedSettings setting) - => setting is { Enabled: false, Priority: 0 } && setting.Settings.Values.All(s => s == 0); - + => setting is { Enabled: false, Priority: 0 } && setting.Settings.Values.All(s => s == 0); + /// private static bool SettingIsDefaultV0(ModSettings? setting) => setting is { Enabled: false, Priority: 0 } && setting.Settings.All(s => s == 0); diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 3dba6903..d85e3256 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -13,7 +13,8 @@ using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; - +using Penumbra.Collections.Cache; + namespace Penumbra.Collections; public partial class ModCollection @@ -24,8 +25,10 @@ public partial class ModCollection public bool HasCache => _cache != null; - // Count the number of changes of the effective file list. - // This is used for material and imc changes. + /// + /// Count the number of changes of the effective file list. + /// This is used for material and imc changes. + /// public int ChangeCounter { get; internal set; } // Only create, do not update. diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs deleted file mode 100644 index 5ed7f801..00000000 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ /dev/null @@ -1,485 +0,0 @@ -using OtterGui; -using OtterGui.Classes; -using Penumbra.Meta.Manager; -using Penumbra.Meta.Manipulations; -using Penumbra.Mods; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Penumbra.Api.Enums; -using Penumbra.String.Classes; -using Penumbra.Mods.Manager; - -namespace Penumbra.Collections; - -public record struct ModPath( IMod Mod, FullPath Path ); -public record ModConflicts( IMod Mod2, List< object > Conflicts, bool HasPriority, bool Solved ); - -/// -/// The Cache contains all required temporary data to use a collection. -/// It will only be setup if a collection gets activated in any way. -/// -public class ModCollectionCache : 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 - { - get - { - SetChangedItems(); - return _changedItems; - } - } - - // The cache reacts through events on its collection changing. - public ModCollectionCache( ModCollection collection ) - { - _collection = collection; - MetaManipulations = new MetaManager( _collection ); - } - - public void Dispose() - { - MetaManipulations.Dispose(); - } - - // 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 ) ) - { - ret[ idx ].Add( game ); - } - } - - return ret; - } - - 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 ); - } - } - } - - if( addMetaChanges ) - { - ++_collection.ChangeCounter; - if( _collection == Penumbra.CollectionManager.Active.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 ) ) ) - { - AddSubMod( option, mod ); - } - - break; - } - } - } - } - - AddSubMod( mod.Default, mod ); - - if( addMetaChanges ) - { - ++_collection.ChangeCounter; - if(Penumbra.ModCaches[mod.Index].TotalManipulations > 0 ) - { - AddMetaFiles(); - } - - if( _collection == Penumbra.CollectionManager.Active.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 ); - } - - 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 ) - { - ModCacheManager.ComputeChangedItems(identifier, 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.cs b/Penumbra/Collections/ModCollection.cs index fc0ac1b7..026516cd 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -5,7 +5,8 @@ using System.Diagnostics; using System.Linq; using Penumbra.Mods.Manager; using Penumbra.Util; - +using Penumbra.Collections.Manager; + namespace Penumbra.Collections; /// diff --git a/Penumbra/Collections/ModCollection.File.cs b/Penumbra/Collections/ModCollectionSave.cs similarity index 100% rename from Penumbra/Collections/ModCollection.File.cs rename to Penumbra/Collections/ModCollectionSave.cs diff --git a/Penumbra/Collections/ResolveData.cs b/Penumbra/Collections/ResolveData.cs index c604d572..e0901e98 100644 --- a/Penumbra/Collections/ResolveData.cs +++ b/Penumbra/Collections/ResolveData.cs @@ -16,11 +16,9 @@ public readonly struct ResolveData public bool Valid => _modCollection != null; - public ResolveData() - { - _modCollection = null!; - AssociatedGameObject = nint.Zero; - } + public ResolveData() + : this(null!, nint.Zero) + { } public ResolveData(ModCollection collection, nint gameObject) { diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index dc77ee37..d1227472 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -22,7 +22,8 @@ using Penumbra.Util; using CharacterUtility = Penumbra.Interop.Services.CharacterUtility; using ModFileSystemSelector = Penumbra.UI.ModsTab.ModFileSystemSelector; using Penumbra.Mods.Manager; - +using Penumbra.Collections.Cache; + namespace Penumbra; public class PenumbraNew diff --git a/Penumbra/UI/Tabs/EffectiveTab.cs b/Penumbra/UI/Tabs/EffectiveTab.cs index a578e8d2..d66c680e 100644 --- a/Penumbra/UI/Tabs/EffectiveTab.cs +++ b/Penumbra/UI/Tabs/EffectiveTab.cs @@ -9,6 +9,7 @@ using OtterGui.Classes; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Collections; +using Penumbra.Collections.Cache; using Penumbra.Collections.Manager; using Penumbra.Meta.Manipulations; using Penumbra.Mods;