From 4b036c6c2667e32b9486e748751e2aa527ad928e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 29 May 2022 19:00:34 +0200 Subject: [PATCH] Change cache reloading and conflicts to actually keep the effective mod and not force full recalculations on every change. --- OtterGui | 2 +- Penumbra.GameData/GameData.cs | 62 +- Penumbra/Api/ModsController.cs | 2 +- Penumbra/Api/PenumbraApi.cs | 3 +- Penumbra/Api/SimpleRedirectManager.cs | 7 +- .../Collections/CollectionManager.Active.cs | 28 +- Penumbra/Collections/CollectionManager.cs | 59 +- .../Collections/ModCollection.Cache.Access.cs | 50 +- Penumbra/Collections/ModCollection.Cache.cs | 584 +++++++++++------- Penumbra/Meta/Manager/MetaManager.Cmp.cs | 20 +- Penumbra/Meta/Manager/MetaManager.Eqdp.cs | 23 +- Penumbra/Meta/Manager/MetaManager.Eqp.cs | 20 +- Penumbra/Meta/Manager/MetaManager.Est.cs | 29 +- Penumbra/Meta/Manager/MetaManager.Gmp.cs | 20 +- Penumbra/Meta/Manager/MetaManager.Imc.cs | 37 +- Penumbra/Meta/Manager/MetaManager.cs | 49 +- Penumbra/Mods/Manager/Mod.Manager.BasePath.cs | 1 + Penumbra/Mods/Manager/Mod.Manager.Options.cs | 24 +- Penumbra/Mods/Manager/ModOptionChangeType.cs | 50 ++ Penumbra/Mods/Mod.BasePath.cs | 1 + Penumbra/Mods/Mod.Files.cs | 1 - Penumbra/Mods/Mod.Meta.cs | 9 + Penumbra/Penumbra.cs | 5 +- .../Classes/ModFileSystemSelector.Filters.cs | 4 +- Penumbra/UI/ConfigWindow.ChangedItemsTab.cs | 53 +- Penumbra/UI/ConfigWindow.EffectiveTab.cs | 20 +- Penumbra/UI/ConfigWindow.Misc.cs | 6 +- Penumbra/UI/ConfigWindow.ModPanel.Settings.cs | 13 +- Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs | 53 +- 29 files changed, 778 insertions(+), 457 deletions(-) create mode 100644 Penumbra/Mods/Manager/ModOptionChangeType.cs diff --git a/OtterGui b/OtterGui index d6fcf1f5..3679cb37 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit d6fcf1f53888d5eec8196eaba2dbb3853534d3bf +Subproject commit 3679cb37d5cc04351c064b1372a6eac51c7375a8 diff --git a/Penumbra.GameData/GameData.cs b/Penumbra.GameData/GameData.cs index cb294ca4..55b53aec 100644 --- a/Penumbra.GameData/GameData.cs +++ b/Penumbra.GameData/GameData.cs @@ -7,45 +7,43 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.GameData.Util; -namespace Penumbra.GameData +namespace Penumbra.GameData; + +public static class GameData { - public static class GameData - { - internal static ObjectIdentification? Identification; - internal static readonly GamePathParser GamePathParser = new(); + internal static ObjectIdentification? Identification; + internal static readonly GamePathParser GamePathParser = new(); - public static IObjectIdentifier GetIdentifier( DataManager dataManager, ClientLanguage clientLanguage ) + public static IObjectIdentifier GetIdentifier( DataManager dataManager, ClientLanguage clientLanguage ) + { + Identification ??= new ObjectIdentification( dataManager, clientLanguage ); + return Identification; + } + + public static IObjectIdentifier GetIdentifier() + { + if( Identification == null ) { - Identification ??= new ObjectIdentification( dataManager, clientLanguage ); - return Identification; + throw new Exception( "Object Identification was not initialized." ); } - public static IObjectIdentifier GetIdentifier() - { - if( Identification == null ) - { - throw new Exception( "Object Identification was not initialized." ); - } - - return Identification; - } - - public static IGamePathParser GetGamePathParser() - => GamePathParser; + return Identification; } - public interface IObjectIdentifier - { - public void Identify( IDictionary< string, object? > set, GamePath path ); + public static IGamePathParser GetGamePathParser() + => GamePathParser; +} - public Dictionary< string, object? > Identify( GamePath path ); - public Item? Identify( SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot ); - } +public interface IObjectIdentifier +{ + public void Identify( IDictionary< string, object? > set, GamePath path ); + public Dictionary< string, object? > Identify( GamePath path ); + public Item? Identify( SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot ); +} - public interface IGamePathParser - { - public ObjectType PathToObjectType( GamePath path ); - public GameObjectInfo GetFileInfo( GamePath path ); - public string VfxToKey( GamePath path ); - } +public interface IGamePathParser +{ + public ObjectType PathToObjectType( GamePath path ); + public GameObjectInfo GetFileInfo( GamePath path ); + public string VfxToKey( GamePath path ); } \ No newline at end of file diff --git a/Penumbra/Api/ModsController.cs b/Penumbra/Api/ModsController.cs index c8254c42..a909aab4 100644 --- a/Penumbra/Api/ModsController.cs +++ b/Penumbra/Api/ModsController.cs @@ -36,7 +36,7 @@ public class ModsController : WebApiController { return Penumbra.CollectionManager.Current.ResolvedFiles.ToDictionary( o => o.Key.ToString(), - o => o.Value.FullName + o => o.Value.Path.FullName ) ?? new Dictionary< string, string >(); } diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index cac23ecb..e0d85731 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Reflection; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Logging; @@ -138,7 +139,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi if( collection.HasCache ) { - return collection.ChangedItems; + return collection.ChangedItems.ToDictionary( kvp => kvp.Key, kvp => kvp.Value.Item2 ); } PluginLog.Warning( $"Collection {collectionName} does not exist or is not loaded." ); diff --git a/Penumbra/Api/SimpleRedirectManager.cs b/Penumbra/Api/SimpleRedirectManager.cs index 6b4a45b3..2f3e219f 100644 --- a/Penumbra/Api/SimpleRedirectManager.cs +++ b/Penumbra/Api/SimpleRedirectManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Dalamud.Logging; +using Penumbra.Collections; using Penumbra.GameData.ByteString; using Penumbra.Mods; @@ -22,13 +23,13 @@ public enum RedirectResult public class SimpleRedirectManager { internal readonly Dictionary< Utf8GamePath, (FullPath File, string Tag) > Replacements = new(); - public readonly HashSet< string > AllowedTags = new(); + public readonly HashSet< string > AllowedTags = new(); - public void Apply( IDictionary< Utf8GamePath, FullPath > dict ) + public void Apply( IDictionary< Utf8GamePath, ModPath > dict ) { foreach( var (gamePath, (file, _)) in Replacements ) { - dict.TryAdd( gamePath, file ); + dict.TryAdd( gamePath, new ModPath(Mod.ForcedFiles, file) ); } } diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 519936a2..d21ad8f9 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -59,7 +59,7 @@ public partial class ModCollection var newCollection = this[ newIdx ]; if( newIdx > Empty.Index ) { - newCollection.CreateCache( false ); + newCollection.CreateCache(); } RemoveCache( oldCollectionIdx ); @@ -122,7 +122,7 @@ public partial class ModCollection var configChanged = !ReadActiveCollections( out var jObject ); // Load the default collection. - var defaultName = jObject[ nameof( Default ) ]?.ToObject< string >() ?? (configChanged ? DefaultCollection : Empty.Name); + var defaultName = jObject[ nameof( Default ) ]?.ToObject< string >() ?? ( configChanged ? DefaultCollection : Empty.Name ); var defaultIdx = GetIndexForCollectionName( defaultName ); if( defaultIdx < 0 ) { @@ -250,12 +250,12 @@ public partial class ModCollection // Cache handling. private void CreateNecessaryCaches() { - Default.CreateCache( true ); - Current.CreateCache( false ); + Default.CreateCache(); + Current.CreateCache(); foreach( var collection in _characters.Values ) { - collection.CreateCache( false ); + collection.CreateCache(); } } @@ -268,27 +268,27 @@ public partial class ModCollection } // Recalculate effective files for active collections on events. - private void OnModAddedActive( bool meta ) + private void OnModAddedActive( Mod mod ) { - foreach( var collection in this.Where( c => c.HasCache && c[ ^1 ].Settings?.Enabled == true ) ) + foreach( var collection in this.Where( c => c.HasCache && c[ mod.Index ].Settings?.Enabled == true ) ) { - collection.CalculateEffectiveFileList( meta, collection == Penumbra.CollectionManager.Default ); + collection._cache!.AddMod( mod, true ); } } - private void OnModRemovedActive( bool meta, IEnumerable< ModSettings? > settings ) + private void OnModRemovedActive( Mod mod ) { - foreach( var (collection, _) in this.Zip( settings ).Where( c => c.First.HasCache && c.Second?.Enabled == true ) ) + foreach( var collection in this.Where( c => c.HasCache && c[ mod.Index ].Settings?.Enabled == true ) ) { - collection.CalculateEffectiveFileList( meta, collection == Penumbra.CollectionManager.Default ); + collection._cache!.RemoveMod( mod, true ); } } - private void OnModChangedActive( bool meta, int modIdx ) + private void OnModMovedActive( Mod mod ) { - foreach( var collection in this.Where( c => c.HasCache && c[ modIdx ].Settings?.Enabled == true ) ) + foreach( var collection in this.Where( c => c.HasCache && c[ mod.Index ].Settings?.Enabled == true ) ) { - collection.CalculateEffectiveFileList( meta, collection == Penumbra.CollectionManager.Default ); + collection._cache!.ReloadMod( mod, true ); } } } diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 13b0fdf3..36bb2599 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -204,7 +204,7 @@ public partial class ModCollection // Afterwards, we update the caches. This can not happen in the same loop due to inheritance. foreach( var collection in this ) { - collection.ForceCacheUpdate( collection == Default ); + collection.ForceCacheUpdate(); } } @@ -221,27 +221,29 @@ public partial class ModCollection collection.AddMod( mod ); } - OnModAddedActive( mod.TotalManipulations > 0 ); + OnModAddedActive( mod ); break; case ModPathChangeType.Deleted: - var settings = this.Select( c => c[mod.Index].Settings ).ToList(); + OnModRemovedActive( mod ); foreach( var collection in this ) { collection.RemoveMod( mod, mod.Index ); } - OnModRemovedActive( mod.TotalManipulations > 0, settings ); break; case ModPathChangeType.Moved: + OnModMovedActive( mod ); foreach( var collection in this.Where( collection => collection.Settings[ mod.Index ] != null ) ) { collection.Save(); } - OnModChangedActive( mod.TotalManipulations > 0, mod.Index ); + break; + case ModPathChangeType.StartingReload: + OnModRemovedActive( mod ); break; case ModPathChangeType.Reloaded: - OnModChangedActive( mod.TotalManipulations > 0, mod.Index ); + OnModAddedActive( mod ); break; default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); } @@ -252,25 +254,24 @@ public partial class ModCollection // And also updating effective file and meta manipulation lists if necessary. private void OnModOptionsChanged( ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx ) { - var (handleChanges, recomputeList, withMeta) = type switch + // Handle changes that break revertability. + if( type == ModOptionChangeType.PrepareChange ) { - ModOptionChangeType.GroupRenamed => ( true, false, false ), - ModOptionChangeType.GroupAdded => ( true, false, false ), - ModOptionChangeType.GroupDeleted => ( true, true, true ), - ModOptionChangeType.GroupMoved => ( true, false, false ), - ModOptionChangeType.GroupTypeChanged => ( true, true, true ), - ModOptionChangeType.PriorityChanged => ( true, true, true ), - ModOptionChangeType.OptionAdded => ( true, true, true ), - ModOptionChangeType.OptionDeleted => ( true, true, true ), - ModOptionChangeType.OptionMoved => ( true, false, false ), - ModOptionChangeType.OptionFilesChanged => ( false, true, false ), - ModOptionChangeType.OptionSwapsChanged => ( false, true, false ), - ModOptionChangeType.OptionMetaChanged => ( false, true, true ), - ModOptionChangeType.DisplayChange => ( false, false, false ), - _ => ( false, false, false ), - }; + foreach( var collection in this.Where( c => c.HasCache ) ) + { + if( collection[ mod.Index ].Settings is { Enabled: true } ) + { + collection._cache!.RemoveMod( mod, false ); + } + } - if( handleChanges ) + 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 ) { @@ -281,14 +282,22 @@ public partial class ModCollection } } + // 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 ) { - // TODO: Does not check if the option that was changed is actually enabled. foreach( var collection in this.Where( c => c.HasCache ) ) { if( collection[ mod.Index ].Settings is { Enabled: true } ) { - collection.CalculateEffectiveFileList( withMeta, collection == Penumbra.CollectionManager.Default ); + if( reload ) + { + collection._cache!.ReloadMod( mod, true ); + } + else + { + collection._cache!.AddMod( mod, true ); + } } } } diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index 8de2e1cc..2e3bda92 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -6,6 +6,7 @@ using Dalamud.Logging; using OtterGui.Classes; using Penumbra.GameData.ByteString; using Penumbra.Meta.Manager; +using Penumbra.Mods; namespace Penumbra.Collections; @@ -18,21 +19,21 @@ public partial class ModCollection => _cache != null; public int RecomputeCounter - => _cache?.RecomputeCounter ?? 0; + => _cache?.ChangeCounter ?? 0; // Only create, do not update. - private void CreateCache( bool isDefault ) + private void CreateCache() { if( _cache == null ) { - CalculateEffectiveFileList( true, isDefault ); + CalculateEffectiveFileList(); PluginLog.Verbose( "Created new cache for collection {Name:l}.", Name ); } } // Force an update with metadata for this cache. - private void ForceCacheUpdate( bool isDefault ) - => CalculateEffectiveFileList( true, isDefault ); + private void ForceCacheUpdate() + => CalculateEffectiveFileList(); // Clear the current cache. @@ -49,7 +50,7 @@ public partial class ModCollection // Force a file to be resolved to a specific path regardless of conflicts. internal void ForceFile( Utf8GamePath path, FullPath fullPath ) - => _cache!.ResolvedFiles[ path ] = fullPath; + => _cache!.ResolvedFiles[ path ] = new ModPath( Mod.ForcedFiles, fullPath ); // Force a file resolve to be removed. internal void RemoveFile( Utf8GamePath path ) @@ -59,25 +60,25 @@ public partial class ModCollection internal MetaManager? MetaCache => _cache?.MetaManipulations; - internal IReadOnlyDictionary< Utf8GamePath, FullPath > ResolvedFiles - => _cache?.ResolvedFiles ?? new Dictionary< Utf8GamePath, FullPath >(); + internal IReadOnlyDictionary< Utf8GamePath, ModPath > ResolvedFiles + => _cache?.ResolvedFiles ?? new Dictionary< Utf8GamePath, ModPath >(); - internal IReadOnlyDictionary< string, object? > ChangedItems - => _cache?.ChangedItems ?? new Dictionary< string, object? >(); + internal IReadOnlyDictionary< string, (SingleArray< Mod >, object?) > ChangedItems + => _cache?.ChangedItems ?? new Dictionary< string, (SingleArray< Mod >, object?) >(); - internal IReadOnlyList< ConflictCache.Conflict > Conflicts - => _cache?.Conflicts.Conflicts ?? Array.Empty< ConflictCache.Conflict >(); + internal IEnumerable< SingleArray< ModConflicts > > AllConflicts + => _cache?.AllConflicts ?? Array.Empty< SingleArray< ModConflicts > >(); - internal SubList< ConflictCache.Conflict > ModConflicts( int modIdx ) - => _cache?.Conflicts.ModConflicts( modIdx ) ?? SubList< ConflictCache.Conflict >.Empty; + internal SingleArray< ModConflicts > Conflicts( Mod mod ) + => _cache?.Conflicts( mod ) ?? new SingleArray< ModConflicts >(); // Update the effective file list for the given cache. // Creates a cache if necessary. - public void CalculateEffectiveFileList( bool withMetaManipulations, bool reloadDefault ) + public void CalculateEffectiveFileList() => Penumbra.Framework.RegisterImportant( nameof( CalculateEffectiveFileList ) + Name, - () => CalculateEffectiveFileListInternal( withMetaManipulations, reloadDefault ) ); + CalculateEffectiveFileListInternal ); - private void CalculateEffectiveFileListInternal( bool withMetaManipulations, bool reloadDefault ) + private void CalculateEffectiveFileListInternal() { // Skip the empty collection. if( Index == 0 ) @@ -85,16 +86,13 @@ public partial class ModCollection return; } - PluginLog.Debug( "[{Thread}] Recalculating effective file list for {CollectionName:l} [{WithMetaManipulations}] [{ReloadDefault}]", Thread.CurrentThread.ManagedThreadId, Name, - withMetaManipulations, reloadDefault ); + PluginLog.Debug( "[{Thread}] Recalculating effective file list for {CollectionName:l}", + Thread.CurrentThread.ManagedThreadId, Name ); _cache ??= new Cache( this ); - _cache.CalculateEffectiveFileList( withMetaManipulations ); - if( reloadDefault ) - { - SetFiles(); - Penumbra.ResidentResources.Reload(); - } - PluginLog.Debug( "[{Thread}] Recalculation of effective file list for {CollectionName:l} finished.", Thread.CurrentThread.ManagedThreadId, Name); + _cache.FullRecalculation(); + + PluginLog.Debug( "[{Thread}] Recalculation of effective file list for {CollectionName:l} finished.", + Thread.CurrentThread.ManagedThreadId, Name ); } // Set Metadata files. diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 40704785..d7c15b22 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -1,38 +1,44 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using System.Threading; using Dalamud.Logging; -using Dalamud.Utility; +using OtterGui.Classes; using Penumbra.GameData.ByteString; using Penumbra.Meta.Manager; using Penumbra.Meta.Manipulations; using Penumbra.Mods; +using Penumbra.Util; namespace Penumbra.Collections; +public record struct ModPath( Mod Mod, FullPath Path ); +public record ModConflicts( Mod 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. private class Cache : IDisposable { - private readonly Dictionary< Utf8GamePath, FileRegister > _registeredFiles = new(); - private readonly Dictionary< MetaManipulation, FileRegister > _registeredManipulations = new(); + private readonly ModCollection _collection; + private readonly SortedList< string, (SingleArray< Mod >, object?) > _changedItems = new(); + public readonly Dictionary< Utf8GamePath, ModPath > ResolvedFiles = new(); + public readonly MetaManager MetaManipulations; + private readonly Dictionary< Mod, SingleArray< ModConflicts > > _conflicts = new(); - private readonly ModCollection _collection; - private readonly SortedList< string, object? > _changedItems = new(); - public readonly Dictionary< Utf8GamePath, FullPath > ResolvedFiles = new(); - public readonly MetaManager MetaManipulations; - public ConflictCache Conflicts = new(); + public IEnumerable< SingleArray< ModConflicts > > AllConflicts + => _conflicts.Values; - // Count the number of recalculations of the effective file list. + public SingleArray< ModConflicts > Conflicts( Mod mod ) + => _conflicts.TryGetValue( mod, out var c ) ? c : new SingleArray< ModConflicts >(); + + // Count the number of changes of the effective file list. // This is used for material and imc changes. - public int RecomputeCounter { get; private set; } = 0; + public int ChangeCounter { get; private set; } + private int _changedItemsSaveCounter = -1; // Obtain currently changed items. Computes them if they haven't been computed before. - public IReadOnlyDictionary< string, object? > ChangedItems + public IReadOnlyDictionary< string, (SingleArray< Mod >, object?) > ChangedItems { get { @@ -64,85 +70,376 @@ public partial class ModCollection return null; } - if( candidate.InternalName.Length > Utf8GamePath.MaxGamePathLength - || candidate.IsRooted && !candidate.Exists ) + if( candidate.Path.InternalName.Length > Utf8GamePath.MaxGamePathLength + || candidate.Path.IsRooted && !candidate.Path.Exists ) { return null; } - return candidate; + return candidate.Path; } private void OnModSettingChange( ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool _ ) { - // Recompute the file list if it was not just a non-conflicting priority change - // or a setting change for a disabled mod. - if( type == ModSettingChange.Priority && !Conflicts.ModConflicts( modIdx ).Any() - || type == ModSettingChange.Setting && !_collection[ modIdx ].Settings!.Enabled ) + var mod = Penumbra.ModManager[ modIdx ]; + switch( type ) { - return; - } + case ModSettingChange.Inheritance: + ReloadMod( mod, true ); + break; + case ModSettingChange.EnableState: + if( oldValue != 1 ) + { + AddMod( mod, true ); + } + else + { + RemoveMod( mod, true ); + } - var hasMeta = type is ModSettingChange.MultiEnableState or ModSettingChange.MultiInheritance - || Penumbra.ModManager[ modIdx ].AllManipulations.Any(); - _collection.CalculateEffectiveFileList( hasMeta, Penumbra.CollectionManager.Default == _collection ); + break; + case ModSettingChange.Priority: + if( Conflicts( mod ).Count > 0 ) + { + ReloadMod( mod, true ); + } + + break; + case ModSettingChange.Setting: + if( _collection[ modIdx ].Settings?.Enabled == true ) + { + ReloadMod( mod, true ); + } + + break; + case ModSettingChange.MultiInheritance: + case ModSettingChange.MultiEnableState: + FullRecalculation(); + break; + } } // Inheritance changes are too big to check for relevance, // just recompute everything. private void OnInheritanceChange( bool _ ) - => _collection.CalculateEffectiveFileList( true, true ); + => FullRecalculation(); - // Clear all local and global caches to prepare for recomputation. - private void ClearStorageAndPrepare() + public void FullRecalculation() { - _changedItems.Clear(); - _registeredFiles.EnsureCapacity( 2 * ResolvedFiles.Count ); ResolvedFiles.Clear(); - Conflicts.ClearFileConflicts(); - } + MetaManipulations.Reset(); + _conflicts.Clear(); - // Recalculate all file changes from current settings. Include all fixed custom redirects. - // Recalculate meta manipulations only if withManipulations is true. - public void CalculateEffectiveFileList( bool withManipulations ) - { - ClearStorageAndPrepare(); - if( withManipulations ) - { - _registeredManipulations.EnsureCapacity( 2 * MetaManipulations.Count ); - MetaManipulations.Reset(); - } + // Add all forced redirects. + Penumbra.Redirects.Apply( ResolvedFiles ); - AddCustomRedirects(); - for( var i = 0; i < Penumbra.ModManager.Count; ++i ) + foreach( var mod in Penumbra.ModManager ) { - AddMod( i, withManipulations ); + AddMod( mod, false ); } AddMetaFiles(); - ++RecomputeCounter; - _registeredFiles.Clear(); - _registeredFiles.TrimExcess(); - _registeredManipulations.Clear(); - _registeredManipulations.TrimExcess(); + + ++ChangeCounter; + + if( _collection == Penumbra.CollectionManager.Default ) + { + Penumbra.ResidentResources.Reload(); + MetaManipulations.SetFiles(); + } } + public void ReloadMod( Mod mod, bool addMetaChanges ) + { + RemoveMod( mod, addMetaChanges ); + AddMod( mod, addMetaChanges ); + } + + public void RemoveMod( Mod 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 ) + { + ++ChangeCounter; + if( _collection == Penumbra.CollectionManager.Default ) + { + 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( Mod mod, bool addMetaChanges ) + { + var settings = _collection[ mod.Index ].Settings; + if( settings is not { Enabled: true } ) + { + return; + } + + AddSubMod( mod.Default, mod ); + 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 SelectType.Single: + AddSubMod( group[ ( int )config ], mod ); + break; + case SelectType.Multi: + { + foreach( var (option, _) in group.WithIndex() + .OrderByDescending( p => group.OptionPriority( p.Item2 ) ) + .Where( p => ( ( 1 << p.Item2 ) & config ) != 0 ) ) + { + AddSubMod( option, mod ); + } + + break; + } + } + } + + if( addMetaChanges ) + { + ++ChangeCounter; + if( _collection == Penumbra.CollectionManager.Default ) + { + Penumbra.ResidentResources.Reload(); + MetaManipulations.SetFiles(); + } + + if( mod.TotalManipulations > 0 ) + { + AddMetaFiles(); + } + } + } + + // Add all files and possibly manipulations of a specific submod + private void AddSubMod( ISubMod subMod, Mod parentMod ) + { + foreach( var (path, file) in subMod.Files.Concat( subMod.FileSwaps ) ) + { + // Skip all filtered files + if( Mod.FilterFile( path ) ) + { + continue; + } + + 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, Mod mod ) + { + 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( Mod 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, Mod addedMod, Mod existingMod ) + { + var addedPriority = addedMod.Index >= 0 ? _collection[ addedMod.Index ].Settings!.Priority : int.MaxValue; + var existingPriority = existingMod.Index >= 0 ? _collection[ existingMod.Index ].Settings!.Priority : int.MaxValue; + + 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, Mod 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.Imc.SetFiles(); + // Identify and record all manipulated objects for this entire collection. private void SetChangedItems() { - if( _changedItems.Count > 0 || ResolvedFiles.Count + MetaManipulations.Count == 0 ) + if( _changedItemsSaveCounter == ChangeCounter ) { return; } try { + _changedItemsSaveCounter = 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 = GameData.GameData.GetIdentifier(); - foreach( var resolved in ResolvedFiles.Keys.Where( file => !file.Path.EndsWith( 'i', 'm', 'c' ) ) ) + foreach( var (resolved, modPath) in ResolvedFiles.Where( file => !file.Key.Path.EndsWith( 'i', 'm', 'c' ) ) ) { - identifier.Identify( _changedItems, resolved.ToGamePath() ); + foreach( var (name, obj) in identifier.Identify( resolved.ToGamePath() ) ) + { + if( !_changedItems.TryGetValue( name, out var data ) ) + { + _changedItems.Add( name, ( new SingleArray< Mod >( modPath.Mod ), obj ) ); + } + else if( !data.Item1.Contains( modPath.Mod ) ) + { + _changedItems[ name ] = ( data.Item1.Append( modPath.Mod ), obj ); + } + } } // TODO: Meta Manipulations } @@ -151,186 +448,5 @@ public partial class ModCollection PluginLog.Error( $"Unknown Error:\n{e}" ); } } - - // 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, FileRegister priority ) - { - if( _registeredFiles.TryGetValue( path, out var register ) ) - { - if( register.SameMod( priority, out var less ) ) - { - Conflicts.AddConflict( register.ModIdx, priority.ModIdx, register.ModPriority, priority.ModPriority, path ); - if( less ) - { - _registeredFiles[ path ] = priority; - ResolvedFiles[ path ] = file; - } - } - else - { - // File seen before in the same mod: - // use higher priority or earlier recurrences in case of same priority. - // Do not add conflicts. - if( less ) - { - _registeredFiles[ path ] = priority; - ResolvedFiles[ path ] = file; - } - } - } - else // File not seen before, just add it. - { - _registeredFiles.Add( path, priority ); - ResolvedFiles.Add( path, file ); - } - } - - // 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, FileRegister priority ) - { - if( _registeredManipulations.TryGetValue( manip, out var register ) ) - { - if( register.SameMod( priority, out var less ) ) - { - Conflicts.AddConflict( register.ModIdx, priority.ModIdx, register.ModPriority, priority.ModPriority, manip ); - if( less ) - { - _registeredManipulations[ manip ] = priority; - MetaManipulations.ApplyMod( manip, priority.ModIdx ); - } - } - else - { - // Manipulation seen before in the same mod: - // use higher priority or earlier occurrences in case of same priority. - // Do not add conflicts. - if( less ) - { - _registeredManipulations[ manip ] = priority; - MetaManipulations.ApplyMod( manip, priority.ModIdx ); - } - } - } - else // Manipulation not seen before, just add it. - { - _registeredManipulations[ manip ] = priority; - MetaManipulations.ApplyMod( manip, priority.ModIdx ); - } - } - - // Add all files and possibly manipulations of a specific submod with the given priorities. - private void AddSubMod( ISubMod mod, FileRegister priority, bool withManipulations ) - { - foreach( var (path, file) in mod.Files.Concat( mod.FileSwaps ) ) - { - // Skip all filtered files - if( Mod.FilterFile( path ) ) - { - continue; - } - - AddFile( path, file, priority ); - } - - if( withManipulations ) - { - foreach( var manip in mod.Manipulations ) - { - AddManipulation( manip, priority ); - } - } - } - - // Add all files and possibly manipulations of a given mod according to its settings in this collection. - private void AddMod( int modIdx, bool withManipulations ) - { - var settings = _collection[ modIdx ].Settings; - if( settings is not { Enabled: true } ) - { - return; - } - - var mod = Penumbra.ModManager[ modIdx ]; - AddSubMod( mod.Default, new FileRegister( modIdx, settings.Priority, 0, 0 ), withManipulations ); - for( var idx = 0; idx < mod.Groups.Count; ++idx ) - { - var config = settings.Settings[ idx ]; - var group = mod.Groups[ idx ]; - if( group.Count == 0 ) - { - continue; - } - - switch( group.Type ) - { - case SelectType.Single: - var singlePriority = new FileRegister( modIdx, settings.Priority, group.Priority, group.Priority ); - AddSubMod( group[ ( int )config ], singlePriority, withManipulations ); - break; - case SelectType.Multi: - { - for( var optionIdx = 0; optionIdx < group.Count; ++optionIdx ) - { - if( ( ( 1 << optionIdx ) & config ) != 0 ) - { - var priority = new FileRegister( modIdx, settings.Priority, group.Priority, group.OptionPriority( optionIdx ) ); - AddSubMod( group[ optionIdx ], priority, withManipulations ); - } - } - - break; - } - } - } - } - - // Add all necessary meta file redirects. - private void AddMetaFiles() - => MetaManipulations.Imc.SetFiles(); - - // Add all API redirects. - private void AddCustomRedirects() - { - Penumbra.Redirects.Apply( ResolvedFiles ); - foreach( var gamePath in ResolvedFiles.Keys ) - { - _registeredFiles.Add( gamePath, new FileRegister( -1, int.MaxValue, 0, 0 ) ); - } - } - - - // Struct to keep track of all priorities involved in a mod and register and compare accordingly. - private readonly record struct FileRegister( int ModIdx, int ModPriority, int GroupPriority, int OptionPriority ) - { - public bool SameMod( FileRegister other, out bool less ) - { - if( ModIdx != other.ModIdx ) - { - less = ModPriority < other.ModPriority; - return true; - } - - if( GroupPriority < other.GroupPriority ) - { - less = true; - } - else if( GroupPriority == other.GroupPriority ) - { - less = OptionPriority < other.OptionPriority; - } - else - { - less = false; - } - - return false; - } - }; } } \ No newline at end of file diff --git a/Penumbra/Meta/Manager/MetaManager.Cmp.cs b/Penumbra/Meta/Manager/MetaManager.Cmp.cs index e591a0b6..eae00788 100644 --- a/Penumbra/Meta/Manager/MetaManager.Cmp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Cmp.cs @@ -5,6 +5,7 @@ using System.Linq; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; +using Penumbra.Mods; namespace Penumbra.Meta.Manager; @@ -13,7 +14,7 @@ public partial class MetaManager public struct MetaManagerCmp : IDisposable { public CmpFile? File = null; - public readonly Dictionary< RspManipulation, int > Manipulations = new(); + public readonly Dictionary< RspManipulation, Mod > Manipulations = new(); public MetaManagerCmp() { } @@ -38,10 +39,10 @@ public partial class MetaManager Manipulations.Clear(); } - public bool ApplyMod( RspManipulation m, int modIdx ) + public bool ApplyMod( RspManipulation m, Mod mod ) { #if USE_CMP - Manipulations[ m ] = modIdx; + Manipulations[ m ] = mod; File ??= new CmpFile(); return m.Apply( File ); #else @@ -49,6 +50,19 @@ public partial class MetaManager #endif } + public bool RevertMod( RspManipulation m ) + { +#if USE_CMP + if( Manipulations.Remove( m ) ) + { + var def = CmpFile.GetDefault( m.SubRace, m.Attribute ); + var manip = new RspManipulation( m.SubRace, m.Attribute, def ); + return manip.Apply( File! ); + } +#endif + return false; + } + public void Dispose() { File?.Dispose(); diff --git a/Penumbra/Meta/Manager/MetaManager.Eqdp.cs b/Penumbra/Meta/Manager/MetaManager.Eqdp.cs index 96daf6a0..bb5ec9d0 100644 --- a/Penumbra/Meta/Manager/MetaManager.Eqdp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Eqdp.cs @@ -6,6 +6,7 @@ using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; +using Penumbra.Mods; using Penumbra.Util; namespace Penumbra.Meta.Manager; @@ -14,9 +15,9 @@ public partial class MetaManager { public struct MetaManagerEqdp : IDisposable { - public ExpandedEqdpFile?[] Files = new ExpandedEqdpFile?[CharacterUtility.NumEqdpFiles - 2]; // TODO: female Hrothgar + public readonly ExpandedEqdpFile?[] Files = new ExpandedEqdpFile?[CharacterUtility.NumEqdpFiles - 2]; // TODO: female Hrothgar - public readonly Dictionary< EqdpManipulation, int > Manipulations = new(); + public readonly Dictionary< EqdpManipulation, Mod > Manipulations = new(); public MetaManagerEqdp() { } @@ -50,10 +51,10 @@ public partial class MetaManager Manipulations.Clear(); } - public bool ApplyMod( EqdpManipulation m, int modIdx ) + public bool ApplyMod( EqdpManipulation m, Mod mod ) { #if USE_EQDP - Manipulations[ m ] = modIdx; + Manipulations[ m ] = mod; var file = Files[ Array.IndexOf( CharacterUtility.EqdpIndices, m.FileIndex() ) ] ??= new ExpandedEqdpFile( Names.CombinedRace( m.Gender, m.Race ), m.Slot.IsAccessory() ); // TODO: female Hrothgar return m.Apply( file ); @@ -62,6 +63,20 @@ public partial class MetaManager #endif } + public bool RevertMod( EqdpManipulation m ) + { +#if USE_EQDP + if( Manipulations.Remove( m ) ) + { + var def = ExpandedEqdpFile.GetDefault( Names.CombinedRace( m.Gender, m.Race ), m.Slot.IsAccessory(), m.SetId ); + var file = Files[ Array.IndexOf( CharacterUtility.EqdpIndices, m.FileIndex() ) ]!; + var manip = new EqdpManipulation( def, m.Slot, m.Gender, m.Race, m.SetId ); + return manip.Apply( file ); + } +#endif + return false; + } + public ExpandedEqdpFile? File( GenderRace race, bool accessory ) => Files[ Array.IndexOf( CharacterUtility.EqdpIndices, CharacterUtility.EqdpIdx( race, accessory ) ) ]; // TODO: female Hrothgar diff --git a/Penumbra/Meta/Manager/MetaManager.Eqp.cs b/Penumbra/Meta/Manager/MetaManager.Eqp.cs index 053e8228..9c980d67 100644 --- a/Penumbra/Meta/Manager/MetaManager.Eqp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Eqp.cs @@ -5,6 +5,7 @@ using System.Linq; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; +using Penumbra.Mods; namespace Penumbra.Meta.Manager; @@ -13,7 +14,7 @@ public partial class MetaManager public struct MetaManagerEqp : IDisposable { public ExpandedEqpFile? File = null; - public readonly Dictionary< EqpManipulation, int > Manipulations = new(); + public readonly Dictionary< EqpManipulation, Mod > Manipulations = new(); public MetaManagerEqp() { } @@ -38,10 +39,10 @@ public partial class MetaManager Manipulations.Clear(); } - public bool ApplyMod( EqpManipulation m, int modIdx ) + public bool ApplyMod( EqpManipulation m, Mod mod ) { #if USE_EQP - Manipulations[ m ] = modIdx; + Manipulations[ m ] = mod; File ??= new ExpandedEqpFile(); return m.Apply( File ); #else @@ -49,6 +50,19 @@ public partial class MetaManager #endif } + public bool RevertMod( EqpManipulation m ) + { +#if USE_EQP + if( Manipulations.Remove( m ) ) + { + var def = ExpandedEqpFile.GetDefault( m.SetId ); + var manip = new EqpManipulation( def, m.Slot, m.SetId ); + return manip.Apply( File! ); + } +#endif + return false; + } + public void Dispose() { File?.Dispose(); diff --git a/Penumbra/Meta/Manager/MetaManager.Est.cs b/Penumbra/Meta/Manager/MetaManager.Est.cs index c6901f2f..ccc5f926 100644 --- a/Penumbra/Meta/Manager/MetaManager.Est.cs +++ b/Penumbra/Meta/Manager/MetaManager.Est.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; +using Penumbra.Mods; namespace Penumbra.Meta.Manager; @@ -16,7 +18,7 @@ public partial class MetaManager public EstFile? BodyFile = null; public EstFile? HeadFile = null; - public readonly Dictionary< EstManipulation, int > Manipulations = new(); + public readonly Dictionary< EstManipulation, Mod > Manipulations = new(); public MetaManagerEst() { } @@ -49,10 +51,10 @@ public partial class MetaManager Manipulations.Clear(); } - public bool ApplyMod( EstManipulation m, int modIdx ) + public bool ApplyMod( EstManipulation m, Mod mod ) { #if USE_EST - Manipulations[ m ] = modIdx; + Manipulations[ m ] = mod; var file = m.Slot switch { EstManipulation.EstType.Hair => HairFile ??= new EstFile( EstManipulation.EstType.Hair ), @@ -67,6 +69,27 @@ public partial class MetaManager #endif } + public bool RevertMod( EstManipulation m ) + { +#if USE_EST + if( Manipulations.Remove( m ) ) + { + var def = EstFile.GetDefault( m.Slot, Names.CombinedRace( m.Gender, m.Race ), m.SetId ); + var manip = new EstManipulation( m.Gender, m.Race, m.Slot, m.SetId, def ); + var file = m.Slot switch + { + EstManipulation.EstType.Hair => HairFile!, + EstManipulation.EstType.Face => FaceFile!, + EstManipulation.EstType.Body => BodyFile!, + EstManipulation.EstType.Head => HeadFile!, + _ => throw new ArgumentOutOfRangeException(), + }; + return manip.Apply( file ); + } +#endif + return false; + } + public void Dispose() { FaceFile?.Dispose(); diff --git a/Penumbra/Meta/Manager/MetaManager.Gmp.cs b/Penumbra/Meta/Manager/MetaManager.Gmp.cs index 4bdee5c3..49c29076 100644 --- a/Penumbra/Meta/Manager/MetaManager.Gmp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Gmp.cs @@ -5,6 +5,7 @@ using System.Linq; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; +using Penumbra.Mods; namespace Penumbra.Meta.Manager; @@ -13,7 +14,7 @@ public partial class MetaManager public struct MetaManagerGmp : IDisposable { public ExpandedGmpFile? File = null; - public readonly Dictionary< GmpManipulation, int > Manipulations = new(); + public readonly Dictionary< GmpManipulation, Mod > Manipulations = new(); public MetaManagerGmp() { } @@ -37,10 +38,10 @@ public partial class MetaManager } } - public bool ApplyMod( GmpManipulation m, int modIdx ) + public bool ApplyMod( GmpManipulation m, Mod mod ) { #if USE_GMP - Manipulations[ m ] = modIdx; + Manipulations[ m ] = mod; File ??= new ExpandedGmpFile(); return m.Apply( File ); #else @@ -48,6 +49,19 @@ public partial class MetaManager #endif } + public bool RevertMod( GmpManipulation m ) + { +#if USE_GMP + if( Manipulations.Remove( m ) ) + { + var def = ExpandedGmpFile.GetDefault( m.SetId ); + var manip = new GmpManipulation( def, m.SetId ); + return manip.Apply( File! ); + } +#endif + return false; + } + public void Dispose() { File?.Dispose(); diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index ed6457a1..c3858564 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -9,6 +9,7 @@ using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; +using Penumbra.Mods; namespace Penumbra.Meta.Manager; @@ -17,7 +18,7 @@ public partial class MetaManager public readonly struct MetaManagerImc : IDisposable { public readonly Dictionary< Utf8GamePath, ImcFile > Files = new(); - public readonly Dictionary< ImcManipulation, int > Manipulations = new(); + public readonly Dictionary< ImcManipulation, Mod > Manipulations = new(); private readonly ModCollection _collection; private static int _imcManagerCount; @@ -64,10 +65,10 @@ public partial class MetaManager Manipulations.Clear(); } - public bool ApplyMod( ImcManipulation m, int modIdx ) + public bool ApplyMod( ImcManipulation m, Mod mod ) { #if USE_IMC - Manipulations[ m ] = modIdx; + Manipulations[ m ] = mod; var path = m.GamePath(); if( !Files.TryGetValue( path, out var file ) ) { @@ -92,6 +93,36 @@ public partial class MetaManager #endif } + public bool RevertMod( ImcManipulation m ) + { +#if USE_IMC + if( Manipulations.Remove( m ) ) + { + var path = m.GamePath(); + if( !Files.TryGetValue( path, out var file ) ) + { + return false; + } + + var def = ImcFile.GetDefault( path, m.EquipSlot, m.Variant ); + var manip = m with { Entry = def }; + if( !manip.Apply( file ) ) + { + return false; + } + + var fullPath = CreateImcPath( path ); + if( _collection.HasCache ) + { + _collection.ForceFile( path, fullPath ); + } + + return true; + } +#endif + return false; + } + public void Dispose() { foreach( var file in Files.Values ) diff --git a/Penumbra/Meta/Manager/MetaManager.cs b/Penumbra/Meta/Manager/MetaManager.cs index 06c97baf..5d6c0a5b 100644 --- a/Penumbra/Meta/Manager/MetaManager.cs +++ b/Penumbra/Meta/Manager/MetaManager.cs @@ -1,8 +1,10 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using Penumbra.Collections; using Penumbra.Meta.Files; using Penumbra.Meta.Manipulations; +using Penumbra.Mods; namespace Penumbra.Meta.Manager; @@ -28,19 +30,19 @@ public partial class MetaManager : IDisposable } } - public bool TryGetValue( MetaManipulation manip, out int modIdx ) + public bool TryGetValue( MetaManipulation manip, [NotNullWhen(true)] out Mod? mod ) { - modIdx = manip.ManipulationType switch + mod = manip.ManipulationType switch { - MetaManipulation.Type.Eqp => Eqp.Manipulations.TryGetValue( manip.Eqp, out var m ) ? m : -1, - MetaManipulation.Type.Gmp => Gmp.Manipulations.TryGetValue( manip.Gmp, out var m ) ? m : -1, - MetaManipulation.Type.Eqdp => Eqdp.Manipulations.TryGetValue( manip.Eqdp, out var m ) ? m : -1, - MetaManipulation.Type.Est => Est.Manipulations.TryGetValue( manip.Est, out var m ) ? m : -1, - MetaManipulation.Type.Rsp => Cmp.Manipulations.TryGetValue( manip.Rsp, out var m ) ? m : -1, - MetaManipulation.Type.Imc => Imc.Manipulations.TryGetValue( manip.Imc, out var m ) ? m : -1, + MetaManipulation.Type.Eqp => Eqp.Manipulations.TryGetValue( manip.Eqp, out var m ) ? m : null, + MetaManipulation.Type.Gmp => Gmp.Manipulations.TryGetValue( manip.Gmp, out var m ) ? m : null, + MetaManipulation.Type.Eqdp => Eqdp.Manipulations.TryGetValue( manip.Eqdp, out var m ) ? m : null, + MetaManipulation.Type.Est => Est.Manipulations.TryGetValue( manip.Est, out var m ) ? m : null, + MetaManipulation.Type.Rsp => Cmp.Manipulations.TryGetValue( manip.Rsp, out var m ) ? m : null, + MetaManipulation.Type.Imc => Imc.Manipulations.TryGetValue( manip.Imc, out var m ) ? m : null, _ => throw new ArgumentOutOfRangeException(), }; - return modIdx != -1; + return mod != null; } public int Count @@ -84,16 +86,31 @@ public partial class MetaManager : IDisposable Imc.Dispose(); } - public bool ApplyMod( MetaManipulation m, int modIdx ) + public bool ApplyMod( MetaManipulation m, Mod mod ) { return m.ManipulationType switch { - MetaManipulation.Type.Eqp => Eqp.ApplyMod( m.Eqp, modIdx ), - MetaManipulation.Type.Gmp => Gmp.ApplyMod( m.Gmp, modIdx ), - MetaManipulation.Type.Eqdp => Eqdp.ApplyMod( m.Eqdp, modIdx ), - MetaManipulation.Type.Est => Est.ApplyMod( m.Est, modIdx ), - MetaManipulation.Type.Rsp => Cmp.ApplyMod( m.Rsp, modIdx ), - MetaManipulation.Type.Imc => Imc.ApplyMod( m.Imc, modIdx ), + MetaManipulation.Type.Eqp => Eqp.ApplyMod( m.Eqp, mod ), + MetaManipulation.Type.Gmp => Gmp.ApplyMod( m.Gmp, mod ), + MetaManipulation.Type.Eqdp => Eqdp.ApplyMod( m.Eqdp, mod ), + MetaManipulation.Type.Est => Est.ApplyMod( m.Est, mod ), + MetaManipulation.Type.Rsp => Cmp.ApplyMod( m.Rsp, mod ), + MetaManipulation.Type.Imc => Imc.ApplyMod( m.Imc, mod ), + MetaManipulation.Type.Unknown => false, + _ => false, + }; + } + + public bool RevertMod( MetaManipulation m ) + { + return m.ManipulationType switch + { + MetaManipulation.Type.Eqp => Eqp.RevertMod( m.Eqp ), + MetaManipulation.Type.Gmp => Gmp.RevertMod( m.Gmp ), + MetaManipulation.Type.Eqdp => Eqdp.RevertMod( m.Eqdp ), + MetaManipulation.Type.Est => Est.RevertMod( m.Est ), + MetaManipulation.Type.Rsp => Cmp.RevertMod( m.Rsp ), + MetaManipulation.Type.Imc => Imc.RevertMod( m.Imc ), MetaManipulation.Type.Unknown => false, _ => false, }; diff --git a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs index c82f09e6..abb28f0a 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs @@ -81,6 +81,7 @@ public partial class Mod var mod = this[ idx ]; var oldName = mod.Name; + ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath ); if( !mod.Reload( out var metaChange ) ) { PluginLog.Warning( mod.Name.Length == 0 diff --git a/Penumbra/Mods/Manager/Mod.Manager.Options.cs b/Penumbra/Mods/Manager/Mod.Manager.Options.cs index eee4744f..16df63e0 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Options.cs @@ -9,23 +9,6 @@ using Penumbra.Util; namespace Penumbra.Mods; -public enum ModOptionChangeType -{ - GroupRenamed, - GroupAdded, - GroupDeleted, - GroupMoved, - GroupTypeChanged, - PriorityChanged, - OptionAdded, - OptionDeleted, - OptionMoved, - OptionFilesChanged, - OptionSwapsChanged, - OptionMetaChanged, - DisplayChange, -} - public sealed partial class Mod { public sealed partial class Manager @@ -84,6 +67,7 @@ public sealed partial class Mod public void DeleteModGroup( Mod mod, int groupIdx ) { var group = mod._groups[ groupIdx ]; + ModOptionChanged.Invoke( ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1 ); mod._groups.RemoveAt( groupIdx ); group.DeleteFile( mod.ModPath, groupIdx ); ModOptionChanged.Invoke( ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1 ); @@ -221,6 +205,7 @@ public sealed partial class Mod public void DeleteOption( Mod mod, int groupIdx, int optionIdx ) { + ModOptionChanged.Invoke( ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1 ); switch( mod._groups[ groupIdx ] ) { case SingleModGroup s: @@ -252,6 +237,7 @@ public sealed partial class Mod return; } + ModOptionChanged.Invoke( ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1 ); subMod.ManipulationData = manipulations; ModOptionChanged.Invoke( ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1 ); } @@ -264,6 +250,7 @@ public sealed partial class Mod return; } + ModOptionChanged.Invoke( ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1 ); subMod.FileData = replacements; ModOptionChanged.Invoke( ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1 ); } @@ -275,7 +262,7 @@ public sealed partial class Mod subMod.FileData.AddFrom( additions ); if( oldCount != subMod.FileData.Count ) { - ModOptionChanged.Invoke( ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1 ); + ModOptionChanged.Invoke( ModOptionChangeType.OptionFilesAdded, mod, groupIdx, optionIdx, -1 ); } } @@ -287,6 +274,7 @@ public sealed partial class Mod return; } + ModOptionChanged.Invoke( ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1 ); subMod.FileSwapData = swaps; ModOptionChanged.Invoke( ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1 ); } diff --git a/Penumbra/Mods/Manager/ModOptionChangeType.cs b/Penumbra/Mods/Manager/ModOptionChangeType.cs new file mode 100644 index 00000000..080c2983 --- /dev/null +++ b/Penumbra/Mods/Manager/ModOptionChangeType.cs @@ -0,0 +1,50 @@ +namespace Penumbra.Mods; + +public enum ModOptionChangeType +{ + GroupRenamed, + GroupAdded, + GroupDeleted, + GroupMoved, + GroupTypeChanged, + PriorityChanged, + OptionAdded, + OptionDeleted, + OptionMoved, + OptionFilesChanged, + OptionFilesAdded, + OptionSwapsChanged, + OptionMetaChanged, + DisplayChange, + PrepareChange, +} + +public static class ModOptionChangeTypeExtension +{ + // Give information for each type of change. + // If requiresSaving, collections need to be re-saved after this change. + // If requiresReloading, caches need to be manipulated after this change. + // If wasPrepared, caches have already removed the mod beforehand, then need add it again when this event is fired. + // Otherwise, caches need to reload the mod itself. + public static void HandlingInfo( this ModOptionChangeType type, out bool requiresSaving, out bool requiresReloading, out bool wasPrepared ) + { + ( requiresSaving, requiresReloading, wasPrepared ) = type switch + { + ModOptionChangeType.GroupRenamed => ( true, false, false ), + ModOptionChangeType.GroupAdded => ( true, false, false ), + ModOptionChangeType.GroupDeleted => ( true, true, false ), + ModOptionChangeType.GroupMoved => ( true, false, false ), + ModOptionChangeType.GroupTypeChanged => ( true, true, true ), + ModOptionChangeType.PriorityChanged => ( true, true, true ), + ModOptionChangeType.OptionAdded => ( true, true, true ), + ModOptionChangeType.OptionDeleted => ( true, true, false ), + ModOptionChangeType.OptionMoved => ( true, false, false ), + ModOptionChangeType.OptionFilesChanged => ( false, true, false ), + ModOptionChangeType.OptionFilesAdded => ( false, true, true ), + ModOptionChangeType.OptionSwapsChanged => ( false, true, false ), + ModOptionChangeType.OptionMetaChanged => ( false, true, false ), + ModOptionChangeType.DisplayChange => ( false, false, false ), + _ => ( false, false, false ), + }; + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod.BasePath.cs b/Penumbra/Mods/Mod.BasePath.cs index 16efdf12..43581735 100644 --- a/Penumbra/Mods/Mod.BasePath.cs +++ b/Penumbra/Mods/Mod.BasePath.cs @@ -9,6 +9,7 @@ public enum ModPathChangeType Deleted, Moved, Reloaded, + StartingReload, } public partial class Mod diff --git a/Penumbra/Mods/Mod.Files.cs b/Penumbra/Mods/Mod.Files.cs index a10df895..439634c5 100644 --- a/Penumbra/Mods/Mod.Files.cs +++ b/Penumbra/Mods/Mod.Files.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text.RegularExpressions; using Dalamud.Logging; using Newtonsoft.Json.Linq; using Penumbra.GameData.ByteString; diff --git a/Penumbra/Mods/Mod.Meta.cs b/Penumbra/Mods/Mod.Meta.cs index fad75f3d..3e052b3c 100644 --- a/Penumbra/Mods/Mod.Meta.cs +++ b/Penumbra/Mods/Mod.Meta.cs @@ -24,6 +24,12 @@ public enum MetaChangeType : ushort public sealed partial class Mod { + public static readonly Mod ForcedFiles = new(new DirectoryInfo( "." )) + { + Name = "Forced Files", + Index = -1, + }; + public const uint CurrentFileVersion = 1; public uint FileVersion { get; private set; } = CurrentFileVersion; public LowerString Name { get; private set; } = "New Mod"; @@ -138,4 +144,7 @@ public sealed partial class Mod PluginLog.Error( $"Could not write meta file for mod {Name} to {metaFile.FullName}:\n{e}" ); } } + + public override string ToString() + => Name.Text; } \ No newline at end of file diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 6a73e27b..a9bd7c7b 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -432,8 +432,9 @@ public class Penumbra : IDisposable + "> **`Enabled Mods: `** {3}\n" + "> **`Total Conflicts: `** {4}\n" + "> **`Solved Conflicts: `** {5}\n", - CollectionName( c ), c.Index, c.Inheritance.Count, c.ActualSettings.Count( s => s is { Enabled: true } ), c.Conflicts.Count, - c.Conflicts.Count( con => con.Solved ) ); + CollectionName( c ), c.Index, c.Inheritance.Count, c.ActualSettings.Count( s => s is { Enabled: true } ), + c.AllConflicts.SelectMany( x => x ).Sum( x => x.HasPriority ? 0 : x.Conflicts.Count ), + c.AllConflicts.SelectMany( x => x ).Sum( x => x.HasPriority || !x.Solved ? 0 : x.Conflicts.Count ) ); sb.AppendLine( "**Collections**" ); sb.AppendFormat( "> **`#Collections: `** {0}\n", CollectionManager.Count ); diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs index 2b342208..cc10eb12 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs @@ -119,7 +119,7 @@ public partial class ModFileSystemSelector return collection != Penumbra.CollectionManager.Current ? ColorId.InheritedDisabledMod.Value() : ColorId.DisabledMod.Value(); } - var conflicts = Penumbra.CollectionManager.Current.ModConflicts( mod.Index ); + var conflicts = Penumbra.CollectionManager.Current.Conflicts( mod ); if( conflicts.Count == 0 ) { return collection != Penumbra.CollectionManager.Current ? ColorId.InheritedMod.Value() : ColorId.EnabledMod.Value(); @@ -188,7 +188,7 @@ public partial class ModFileSystemSelector } // Conflicts can only be relevant if the mod is enabled. - var conflicts = Penumbra.CollectionManager.Current.ModConflicts( mod.Index ); + var conflicts = Penumbra.CollectionManager.Current.Conflicts( mod ); if( conflicts.Count > 0 ) { if( conflicts.Any( c => !c.Solved ) ) diff --git a/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs b/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs index aa7b6c1e..96253526 100644 --- a/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs +++ b/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs @@ -1,28 +1,52 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Numerics; +using Dalamud.Interface; using ImGuiNET; +using Lumina.Data.Parsing; +using Lumina.Excel.GeneratedSheets; using OtterGui; using OtterGui.Classes; using OtterGui.Raii; +using Penumbra.Mods; +using Penumbra.UI.Classes; namespace Penumbra.UI; public partial class ConfigWindow { - private LowerString _changedItemFilter = LowerString.Empty; + private LowerString _changedItemFilter = LowerString.Empty; + private LowerString _changedItemModFilter = LowerString.Empty; // Draw a simple clipped table containing all changed items. private void DrawChangedItemTab() { // Functions in here for less pollution. - bool FilterChangedItem( KeyValuePair< string, object? > item ) - => item.Key.Contains( _changedItemFilter.Lower, StringComparison.InvariantCultureIgnoreCase ); + bool FilterChangedItem( KeyValuePair< string, (SingleArray< Mod >, object?) > item ) + => ( _changedItemFilter.IsEmpty || item.Key.Contains( _changedItemFilter.Lower, StringComparison.InvariantCultureIgnoreCase ) ) + && ( _changedItemModFilter.IsEmpty || item.Value.Item1.Any( m => m.Name.Contains( _changedItemModFilter ) ) ); - void DrawChangedItemColumn( KeyValuePair< string, object? > item ) + void DrawChangedItemColumn( KeyValuePair< string, (SingleArray< Mod >, object?) > item ) { ImGui.TableNextColumn(); - DrawChangedItem( item.Key, item.Value, ImGui.GetStyle().ScrollbarSize ); + DrawChangedItem( item.Key, item.Value.Item2, false ); + ImGui.TableNextColumn(); + if( item.Value.Item1.Count > 0 ) + { + ImGui.TextUnformatted( item.Value.Item1[ 0 ].Name ); + if( item.Value.Item1.Count > 1 ) + { + ImGuiUtil.HoverTooltip( string.Join( "\n", item.Value.Item1.Skip( 1 ).Select( m => m.Name ) ) ); + } + } + + ImGui.TableNextColumn(); + if( item.Value.Item2 is Item it ) + { + using var color = ImRaii.PushColor( ImGuiCol.Text, ColorId.ItemId.Value() ); + ImGuiUtil.RightAlign( $"({( ( Quad )it.ModelMain ).A})" ); + } } using var tab = ImRaii.TabItem( "Changed Items" ); @@ -32,8 +56,14 @@ public partial class ConfigWindow } // Draw filters. - ImGui.SetNextItemWidth( -1 ); - LowerString.InputWithHint( "##changedItemsFilter", "Filter...", ref _changedItemFilter, 64 ); + var varWidth = ImGui.GetContentRegionAvail().X + - 400 * ImGuiHelpers.GlobalScale + - ImGui.GetStyle().ItemSpacing.X; + ImGui.SetNextItemWidth( 400 * ImGuiHelpers.GlobalScale ); + LowerString.InputWithHint( "##changedItemsFilter", "Filter Item...", ref _changedItemFilter, 128 ); + ImGui.SameLine(); + ImGui.SetNextItemWidth( varWidth ); + LowerString.InputWithHint( "##changedItemsModFilter", "Filter Mods...", ref _changedItemModFilter, 128 ); using var child = ImRaii.Child( "##changedItemsChild", -Vector2.One ); if( !child ) @@ -44,14 +74,19 @@ public partial class ConfigWindow // Draw table of changed items. var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y; var skips = ImGuiClip.GetNecessarySkips( height ); - using var list = ImRaii.Table( "##changedItems", 1, ImGuiTableFlags.RowBg, -Vector2.One ); + using var list = ImRaii.Table( "##changedItems", 3, ImGuiTableFlags.RowBg, -Vector2.One ); if( !list ) { return; } + const ImGuiTableColumnFlags flags = ImGuiTableColumnFlags.NoResize | ImGuiTableColumnFlags.WidthFixed; + ImGui.TableSetupColumn( "items", flags, 400 * ImGuiHelpers.GlobalScale ); + ImGui.TableSetupColumn( "mods", flags, varWidth - 100 * ImGuiHelpers.GlobalScale ); + ImGui.TableSetupColumn( "id", flags, 100 * ImGuiHelpers.GlobalScale ); + var items = Penumbra.CollectionManager.Default.ChangedItems; - var rest = _changedItemFilter.IsEmpty + var rest = _changedItemFilter.IsEmpty && _changedItemModFilter.IsEmpty ? ImGuiClip.ClippedDraw( items, skips, DrawChangedItemColumn, items.Count ) : ImGuiClip.FilteredClippedDraw( items, skips, FilterChangedItem, DrawChangedItemColumn ); ImGuiClip.DrawEndDummy( rest, height ); diff --git a/Penumbra/UI/ConfigWindow.EffectiveTab.cs b/Penumbra/UI/ConfigWindow.EffectiveTab.cs index 241a22bb..4d96423c 100644 --- a/Penumbra/UI/ConfigWindow.EffectiveTab.cs +++ b/Penumbra/UI/ConfigWindow.EffectiveTab.cs @@ -8,6 +8,7 @@ using OtterGui.Classes; using OtterGui.Raii; using Penumbra.Collections; using Penumbra.GameData.ByteString; +using Penumbra.Mods; namespace Penumbra.UI; @@ -111,7 +112,7 @@ public partial class ConfigWindow { // We can treat all meta manipulations the same, // we are only really interested in their ToString function here. - static (object, int) Convert< T >( KeyValuePair< T, int > kvp ) + static (object, Mod) Convert< T >( KeyValuePair< T, Mod > kvp ) => ( kvp.Key!, kvp.Value ); var it = m.Cmp.Manipulations.Select( Convert ) @@ -124,7 +125,7 @@ public partial class ConfigWindow // Filters mean we can not use the known counts. if( hasFilters ) { - var it2 = it.Select( p => ( p.Item1.ToString() ?? string.Empty, Penumbra.ModManager[ p.Item2 ].Name ) ); + var it2 = it.Select( p => ( p.Item1.ToString() ?? string.Empty, p.Item2.Name ) ); if( stop >= 0 ) { ImGuiClip.DrawEndDummy( stop + it2.Count( CheckFilters ), height ); @@ -155,7 +156,7 @@ public partial class ConfigWindow } // Draw a line for a game path and its redirected file. - private static void DrawLine( KeyValuePair< Utf8GamePath, FullPath > pair ) + private static void DrawLine( KeyValuePair< Utf8GamePath, ModPath > pair ) { var (path, name) = pair; ImGui.TableNextColumn(); @@ -164,7 +165,8 @@ public partial class ConfigWindow ImGui.TableNextColumn(); ImGuiUtil.PrintIcon( FontAwesomeIcon.LongArrowAltLeft ); ImGui.TableNextColumn(); - CopyOnClickSelectable( name.InternalName ); + CopyOnClickSelectable( name.Path.InternalName ); + ImGuiUtil.HoverTooltip( $"\nChanged by {name.Mod.Name}." ); } // Draw a line for a path and its name. @@ -181,20 +183,20 @@ public partial class ConfigWindow } // Draw a line for a unfiltered/unconverted manipulation and mod-index pair. - private static void DrawLine( (object, int) pair ) + private static void DrawLine( (object, Mod) pair ) { - var (manipulation, modIdx) = pair; + var (manipulation, mod) = pair; ImGui.TableNextColumn(); ImGuiUtil.CopyOnClickSelectable( manipulation.ToString() ?? string.Empty ); ImGui.TableNextColumn(); ImGuiUtil.PrintIcon( FontAwesomeIcon.LongArrowAltLeft ); ImGui.TableNextColumn(); - ImGuiUtil.CopyOnClickSelectable( Penumbra.ModManager[ modIdx ].Name ); + ImGuiUtil.CopyOnClickSelectable( mod.Name ); } // Check filters for file replacements. - private bool CheckFilters( KeyValuePair< Utf8GamePath, FullPath > kvp ) + private bool CheckFilters( KeyValuePair< Utf8GamePath, ModPath > kvp ) { var (gamePath, fullPath) = kvp; if( _effectiveGamePathFilter.Length > 0 && !gamePath.ToString().Contains( _effectiveGamePathFilter.Lower ) ) @@ -202,7 +204,7 @@ public partial class ConfigWindow return false; } - return _effectiveFilePathFilter.Length == 0 || fullPath.FullName.ToLowerInvariant().Contains( _effectiveFilePathFilter.Lower ); + return _effectiveFilePathFilter.Length == 0 || fullPath.Path.FullName.ToLowerInvariant().Contains( _effectiveFilePathFilter.Lower ); } // Check filters for meta manipulations. diff --git a/Penumbra/UI/ConfigWindow.Misc.cs b/Penumbra/UI/ConfigWindow.Misc.cs index d866a8d6..995ad308 100644 --- a/Penumbra/UI/ConfigWindow.Misc.cs +++ b/Penumbra/UI/ConfigWindow.Misc.cs @@ -29,8 +29,8 @@ public partial class ConfigWindow => Text( resource->FileName(), resource->FileNameLength ); // Draw a changed item, invoking the Api-Events for clicks and tooltips. - // Also draw the item Id in grey - private void DrawChangedItem( string name, object? data, float itemIdOffset = 0 ) + // Also draw the item Id in grey if requested + private void DrawChangedItem( string name, object? data, bool drawId ) { var ret = ImGui.Selectable( name ) ? MouseButton.Left : MouseButton.None; ret = ImGui.IsItemClicked( ImGuiMouseButton.Right ) ? MouseButton.Right : ret; @@ -55,7 +55,7 @@ public partial class ConfigWindow } } - if( data is Item it ) + if( data is Item it && drawId ) { ImGui.SameLine( ImGui.GetContentRegionAvail().X ); ImGuiUtil.RightJustify( $"({( ( Quad )it.ModelMain ).A})", ColorId.ItemId.Value() ); diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs index b9d80489..7acbbfb2 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Settings.cs @@ -15,11 +15,11 @@ public partial class ConfigWindow { private partial class ModPanel { - private ModSettings _settings = null!; - private ModCollection _collection = null!; - private bool _emptySetting; - private bool _inherited; - private SubList< ConflictCache.Conflict > _conflicts = SubList< ConflictCache.Conflict >.Empty; + private ModSettings _settings = null!; + private ModCollection _collection = null!; + private bool _emptySetting; + private bool _inherited; + private SingleArray< ModConflicts > _conflicts = new(); private int? _currentPriority; @@ -29,7 +29,7 @@ public partial class ConfigWindow _collection = selector.SelectedSettingCollection; _emptySetting = _settings == ModSettings.Empty; _inherited = _collection != Penumbra.CollectionManager.Current; - _conflicts = Penumbra.CollectionManager.Current.ModConflicts( _mod.Index ); + _conflicts = Penumbra.CollectionManager.Current.Conflicts( _mod ); } // Draw the whole settings tab as well as its contents. @@ -105,7 +105,6 @@ public partial class ConfigWindow ImGui.SetNextItemWidth( 50 * ImGuiHelpers.GlobalScale ); if( ImGui.InputInt( "##Priority", ref priority, 0, 0 ) ) { - _currentPriority = priority; } diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs index 2b995701..7c57ea9a 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Tabs.cs @@ -93,11 +93,11 @@ public partial class ConfigWindow var zipList = ZipList.FromSortedList( _mod.ChangedItems ); var height = ImGui.GetTextLineHeight(); - ImGuiClip.ClippedDraw( zipList, kvp => _window.DrawChangedItem( kvp.Item1, kvp.Item2 ), height ); + ImGuiClip.ClippedDraw( zipList, kvp => _window.DrawChangedItem( kvp.Item1, kvp.Item2, true ), height ); } // If any conflicts exist, show them in this tab. - private void DrawConflictsTab() + private unsafe void DrawConflictsTab() { using var tab = DrawTab( ConflictTabHeader, Tabs.Conflicts ); if( !tab ) @@ -111,45 +111,30 @@ public partial class ConfigWindow return; } - var conflicts = Penumbra.CollectionManager.Current.ModConflicts( _mod.Index ); - Mod? oldBadMod = null; - using var indent = ImRaii.PushIndent( 0f ); - foreach( var conflict in conflicts ) + foreach( var conflict in Penumbra.CollectionManager.Current.Conflicts( _mod ) ) { - var badMod = Penumbra.ModManager[ conflict.Mod2 ]; - if( badMod != oldBadMod ) + if( ImGui.Selectable( conflict.Mod2.Name ) ) { - if( oldBadMod != null ) - { - indent.Pop( 30f ); - } - - if( ImGui.Selectable( badMod.Name ) ) - { - _window._selector.SelectByValue( badMod ); - } - - ImGui.SameLine(); - using var color = ImRaii.PushColor( ImGuiCol.Text, - conflict.Mod1Priority ? ColorId.HandledConflictMod.Value() : ColorId.ConflictingMod.Value() ); - ImGui.TextUnformatted( $"(Priority {Penumbra.CollectionManager.Current[ conflict.Mod2 ].Settings!.Priority})" ); - - indent.Push( 30f ); + _window._selector.SelectByValue( conflict.Mod2 ); } - if( conflict.Data is Utf8GamePath p ) + ImGui.SameLine(); + using( var color = ImRaii.PushColor( ImGuiCol.Text, + conflict.HasPriority ? ColorId.HandledConflictMod.Value() : ColorId.ConflictingMod.Value() ) ) { - unsafe - { - ImGuiNative.igSelectable_Bool( p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero ); - } - } - else if( conflict.Data is MetaManipulation m ) - { - ImGui.Selectable( m.Manipulation?.ToString() ?? string.Empty ); + ImGui.TextUnformatted( $"(Priority {Penumbra.CollectionManager.Current[ conflict.Mod2.Index ].Settings!.Priority})" ); } - oldBadMod = badMod; + using var indent = ImRaii.PushIndent( 30f ); + foreach( var data in conflict.Conflicts ) + { + var _ = data switch + { + Utf8GamePath p => ImGuiNative.igSelectable_Bool( p.Path.Path, 0, ImGuiSelectableFlags.None, Vector2.Zero ) > 0, + MetaManipulation m => ImGui.Selectable( m.Manipulation?.ToString() ?? string.Empty ), + _ => false, + }; + } } }