From d7214cd851c29c2caafd6286acb1f2bdf687996e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 16 Sep 2021 16:29:20 +0200 Subject: [PATCH] Fixes regarding collection settings, change self-conflict behaviour by iterating through options instead of through files, button to clean up collection settings. --- Penumbra/Meta/MetaManager.cs | 6 +- Penumbra/Mods/ModCollection.cs | 6 +- Penumbra/Mods/ModCollectionCache.cs | 219 +++++++++++++++++++++---- Penumbra/Structs/GroupInformation.cs | 2 + Penumbra/UI/Custom/ImGuiUtil.cs | 10 ++ Penumbra/UI/MenuTabs/TabCollections.cs | 51 +++--- Penumbra/UI/MenuTabs/TabDebug.cs | 30 ++++ Penumbra/Util/ArrayExtensions.cs | 11 ++ 8 files changed, 279 insertions(+), 56 deletions(-) diff --git a/Penumbra/Meta/MetaManager.cs b/Penumbra/Meta/MetaManager.cs index 995fcd97..f0d2cad9 100644 --- a/Penumbra/Meta/MetaManager.cs +++ b/Penumbra/Meta/MetaManager.cs @@ -44,7 +44,7 @@ namespace Penumbra.Meta private readonly MetaDefaults _default; private readonly DirectoryInfo _dir; - private readonly ResidentResources _resourceManagement; + private readonly ResidentResources _resourceManagement; private readonly Dictionary< GamePath, FileInfo > _resolvedFiles; private readonly Dictionary< MetaManipulation, Mod.Mod > _currentManipulations = new(); @@ -53,6 +53,10 @@ namespace Penumbra.Meta public IEnumerable< (MetaManipulation, Mod.Mod) > Manipulations => _currentManipulations.Select( kvp => ( kvp.Key, kvp.Value ) ); + public IEnumerable< (GamePath, FileInfo) > Files + => _currentFiles.Where( kvp => kvp.Value.CurrentFile != null ) + .Select( kvp => ( kvp.Key, kvp.Value.CurrentFile! ) ); + public int Count => _currentManipulations.Count; diff --git a/Penumbra/Mods/ModCollection.cs b/Penumbra/Mods/ModCollection.cs index b75d8af6..8b23612d 100644 --- a/Penumbra/Mods/ModCollection.cs +++ b/Penumbra/Mods/ModCollection.cs @@ -109,7 +109,7 @@ namespace Penumbra.Mods } } - public void UpdateSettings() + public void UpdateSettings( bool forceSave ) { if( Cache == null ) { @@ -122,7 +122,7 @@ namespace Penumbra.Mods changes |= mod.FixSettings(); } - if( changes ) + if( forceSave || changes ) { Save(); } @@ -133,7 +133,7 @@ namespace Penumbra.Mods PluginLog.Debug( "Recalculating effective file list for {CollectionName} [{WithMetaManipulations}] [{IsActiveCollection}]", Name, withMetaManipulations, activeCollection ); Cache ??= new ModCollectionCache( Name, modDir ); - UpdateSettings(); + UpdateSettings( false ); Cache.CalculateEffectiveFileList(); if( withMetaManipulations ) { diff --git a/Penumbra/Mods/ModCollectionCache.cs b/Penumbra/Mods/ModCollectionCache.cs index 7eeab9a1..6c8ae654 100644 --- a/Penumbra/Mods/ModCollectionCache.cs +++ b/Penumbra/Mods/ModCollectionCache.cs @@ -1,11 +1,16 @@ -using System; +using System.Collections; using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; using System.IO; using System.Linq; -using Lumina.Data.Parsing; +using System.Runtime.InteropServices; +using Dalamud.Logging; using Penumbra.GameData.Util; using Penumbra.Meta; using Penumbra.Mod; +using Penumbra.Structs; +using Penumbra.Util; namespace Penumbra.Mods { @@ -13,42 +18,206 @@ namespace Penumbra.Mods // It will only be setup if a collection gets activated in any way. public class ModCollectionCache { + // Shared caches to avoid allocations. + private static readonly BitArray FileSeen = new( 256 ); + private static readonly Dictionary< GamePath, Mod.Mod > RegisteredFiles = new( 256 ); + public readonly Dictionary< string, Mod.Mod > AvailableMods = new(); public readonly Dictionary< GamePath, FileInfo > ResolvedFiles = new(); public readonly Dictionary< GamePath, GamePath > SwappedFiles = new(); + public readonly HashSet< FileInfo > MissingFiles = new(); public readonly MetaManager MetaManipulations; public ModCollectionCache( string collectionName, DirectoryInfo tempDir ) => MetaManipulations = new MetaManager( collectionName, ResolvedFiles, tempDir ); - private void AddFiles( Dictionary< GamePath, Mod.Mod > registeredFiles, Mod.Mod mod ) + private static void ResetFileSeen( int size ) { - foreach( var file in mod.Data.Resources.ModFiles ) + if( size < FileSeen.Length ) { - var gamePaths = mod.GetFiles( file ); - foreach( var gamePath in gamePaths ) + FileSeen.Length = size; + FileSeen.SetAll( false ); + } + else + { + FileSeen.SetAll( false ); + FileSeen.Length = size; + } + } + + public void CalculateEffectiveFileList() + { + ResolvedFiles.Clear(); + SwappedFiles.Clear(); + MissingFiles.Clear(); + RegisteredFiles.Clear(); + + foreach( var mod in AvailableMods.Values + .Where( m => m.Settings.Enabled ) + .OrderByDescending( m => m.Settings.Priority ) ) + { + mod.Cache.ClearFileConflicts(); + AddFiles( mod ); + AddSwaps( mod ); + } + + AddMetaFiles(); + } + + + private void AddFiles( Mod.Mod mod ) + { + ResetFileSeen( mod.Data.Resources.ModFiles.Count ); + // Iterate in reverse so that later groups take precedence before earlier ones. + foreach( var group in mod.Data.Meta.Groups.Values.Reverse() ) + { + switch( group.SelectionType ) { - if( !registeredFiles.TryGetValue( gamePath, out var oldMod ) ) + case SelectType.Single: + AddFilesForSingle( group, mod ); + break; + case SelectType.Multi: + AddFilesForMulti( group, mod ); + break; + default: throw new InvalidEnumArgumentException(); + } + } + + AddRemainingFiles( mod ); + } + + private void AddFile( Mod.Mod mod, GamePath gamePath, FileInfo file ) + { + if( !RegisteredFiles.TryGetValue( gamePath, out var oldMod ) ) + { + RegisteredFiles.Add( gamePath, mod ); + ResolvedFiles[ gamePath ] = file; + } + else + { + mod.Cache.AddConflict( oldMod, gamePath ); + } + } + + private void AddMissingFile( FileInfo file ) + { + switch( file.Extension.ToLowerInvariant() ) + { + case ".meta": + case ".rgsp": + return; + default: + MissingFiles.Add( file ); + return; + } + } + + private void AddPathsForOption( Option option, Mod.Mod mod, bool enabled ) + { + foreach( var (file, paths) in option.OptionFiles ) + { + var fullPath = Path.Combine( mod.Data.BasePath.FullName, file ); + var idx = mod.Data.Resources.ModFiles.IndexOf( f => f.FullName == fullPath ); + if( idx < 0 ) + { + AddMissingFile( new FileInfo( fullPath ) ); + continue; + } + + var registeredFile = mod.Data.Resources.ModFiles[ idx ]; + registeredFile.Refresh(); + if( !registeredFile.Exists ) + { + AddMissingFile( registeredFile ); + continue; + } + + FileSeen.Set( idx, true ); + if( enabled ) + { + foreach( var path in paths ) { - registeredFiles.Add( gamePath, mod ); - ResolvedFiles[ gamePath ] = file; - } - else - { - mod.Cache.AddConflict( oldMod, gamePath ); + AddFile( mod, path, registeredFile ); } } } } - private void AddSwaps( Dictionary< GamePath, Mod.Mod > registeredFiles, Mod.Mod mod ) + private void AddFilesForSingle( OptionGroup singleGroup, Mod.Mod mod ) + { + Debug.Assert( singleGroup.SelectionType == SelectType.Single ); + + if( !mod.Settings.Settings.TryGetValue( singleGroup.GroupName, out var setting ) ) + { + setting = 0; + } + + for( var i = 0; i < singleGroup.Options.Count; ++i ) + { + AddPathsForOption( singleGroup.Options[ i ], mod, setting == i ); + } + } + + private void AddFilesForMulti( OptionGroup multiGroup, Mod.Mod mod ) + { + Debug.Assert( multiGroup.SelectionType == SelectType.Multi ); + + if( !mod.Settings.Settings.TryGetValue( multiGroup.GroupName, out var setting ) ) + { + return; + } + + // Also iterate options in reverse so that later options take precedence before earlier ones. + for( var i = multiGroup.Options.Count - 1; i >= 0; --i ) + { + AddPathsForOption( multiGroup.Options[ i ], mod, ( setting & ( 1 << i ) ) != 0 ); + } + } + + private void AddRemainingFiles( Mod.Mod mod ) + { + for( var i = 0; i < mod.Data.Resources.ModFiles.Count; ++i ) + { + if( FileSeen.Get( i ) ) + { + continue; + } + + var file = mod.Data.Resources.ModFiles[ i ]; + file.Refresh(); + if( file.Exists ) + { + AddFile( mod, new GamePath( file, mod.Data.BasePath ), file ); + } + else + { + MissingFiles.Add( file ); + } + } + } + + private void AddMetaFiles() + { + foreach( var (gamePath, file) in MetaManipulations.Files ) + { + if( RegisteredFiles.TryGetValue( gamePath, out var mod ) ) + { + PluginLog.Warning( + $"The meta manipulation file {gamePath} was already completely replaced by {mod.Data.Meta.Name}. This is probably a mistake. Using the custom file {file.FullName}." ); + } + + ResolvedFiles[ gamePath ] = file; + } + } + + private void AddSwaps( Mod.Mod mod ) { foreach( var swap in mod.Data.Meta.FileSwaps ) { - if( !registeredFiles.TryGetValue( swap.Key, out var oldMod ) ) + if( !RegisteredFiles.TryGetValue( swap.Key, out var oldMod ) ) { - registeredFiles.Add( swap.Key, mod ); + RegisteredFiles.Add( swap.Key, mod ); SwappedFiles.Add( swap.Key, swap.Value ); } else @@ -86,22 +255,6 @@ namespace Penumbra.Mods MetaManipulations.WriteNewFiles(); } - public void CalculateEffectiveFileList() - { - ResolvedFiles.Clear(); - SwappedFiles.Clear(); - - var registeredFiles = new Dictionary< GamePath, Mod.Mod >(); - foreach( var mod in AvailableMods.Values - .Where( m => m.Settings.Enabled ) - .OrderByDescending( m => m.Settings.Priority ) ) - { - mod.Cache.ClearFileConflicts(); - AddFiles( registeredFiles, mod ); - AddSwaps( registeredFiles, mod ); - } - } - public void RemoveMod( DirectoryInfo basePath ) { if( AvailableMods.TryGetValue( basePath.Name, out var mod ) ) @@ -121,7 +274,7 @@ namespace Penumbra.Mods private class PriorityComparer : IComparer< Mod.Mod > { public int Compare( Mod.Mod? x, Mod.Mod? y ) - => (x?.Settings.Priority ?? 0).CompareTo( y?.Settings.Priority ?? 0 ); + => ( x?.Settings.Priority ?? 0 ).CompareTo( y?.Settings.Priority ?? 0 ); } private static readonly PriorityComparer Comparer = new(); diff --git a/Penumbra/Structs/GroupInformation.cs b/Penumbra/Structs/GroupInformation.cs index 0a912cbf..f9681f11 100644 --- a/Penumbra/Structs/GroupInformation.cs +++ b/Penumbra/Structs/GroupInformation.cs @@ -43,12 +43,14 @@ namespace Penumbra.Structs private bool ApplySingleGroupFiles( RelPath relPath, int selection, HashSet< GamePath > paths ) { + // Selection contains the path, merge all GamePaths for this config. if( Options[ selection ].OptionFiles.TryGetValue( relPath, out var groupPaths ) ) { paths.UnionWith( groupPaths ); return true; } + // If the group contains the file in another selection, return true to skip it for default files. for( var i = 0; i < Options.Count; ++i ) { if( i == selection ) diff --git a/Penumbra/UI/Custom/ImGuiUtil.cs b/Penumbra/UI/Custom/ImGuiUtil.cs index 7570de9d..42e9d8f2 100644 --- a/Penumbra/UI/Custom/ImGuiUtil.cs +++ b/Penumbra/UI/Custom/ImGuiUtil.cs @@ -1,3 +1,4 @@ +using System.Numerics; using System.Security.Cryptography.X509Certificates; using System.Windows.Forms; using Dalamud.Interface; @@ -53,6 +54,15 @@ namespace Penumbra.UI.Custom } } + public static partial class ImGuiCustom + { + public static bool DisableButton( string label, bool condition ) + { + using var alpha = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f, !condition ); + return ImGui.Button( label ) && condition; + } + } + public static partial class ImGuiCustom { public static void PrintIcon( FontAwesomeIcon icon ) diff --git a/Penumbra/UI/MenuTabs/TabCollections.cs b/Penumbra/UI/MenuTabs/TabCollections.cs index ab362bad..56a70aa9 100644 --- a/Penumbra/UI/MenuTabs/TabCollections.cs +++ b/Penumbra/UI/MenuTabs/TabCollections.cs @@ -87,13 +87,25 @@ namespace Penumbra.UI { if( _manager.Collections.AddCollection( _newCollectionName, settings ) ) { - _manager.Collections.SetCurrentCollection( _manager.Collections.Collections[ _newCollectionName ] ); UpdateNames(); + SetCurrentCollection( _manager.Collections.Collections[_newCollectionName], true ); } _newCollectionName = string.Empty; } + private void DrawCleanCollectionButton() + { + if( ImGui.Button( "Clean Settings" ) ) + { + var changes = ModFunctions.CleanUpCollection( _manager.Collections.CurrentCollection.Settings, + _manager.BasePath.EnumerateDirectories() ); + _manager.Collections.CurrentCollection.UpdateSettings( forceSave: changes ); + } + + ImGuiCustom.HoverTooltip( "Remove all stored settings for mods not currently available and fix invalid settings.\nUse at own risk." ); + } + private void DrawNewCollectionInput() { ImGui.InputTextWithHint( "##New Collection", "New Collection", ref _newCollectionName, 64 ); @@ -113,26 +125,31 @@ namespace Penumbra.UI style.Pop(); - if( _manager.Collections.Collections.Count > 1 - && _manager.Collections.CurrentCollection.Name != ModCollection.DefaultCollection ) + var deleteCondition = _manager.Collections.Collections.Count > 1 + && _manager.Collections.CurrentCollection.Name != ModCollection.DefaultCollection; + ImGui.SameLine(); + if( ImGuiCustom.DisableButton( "Delete Current Collection", deleteCondition) ) + { + _manager.Collections.RemoveCollection( _manager.Collections.CurrentCollection.Name ); + SetCurrentCollection( _manager.Collections.CurrentCollection, true ); + UpdateNames(); + } + + if( Penumbra.Config.ShowAdvanced ) { ImGui.SameLine(); - if( ImGui.Button( "Delete Current Collection" ) ) - { - _manager.Collections.RemoveCollection( _manager.Collections.CurrentCollection.Name ); - UpdateNames(); - } + DrawCleanCollectionButton(); } } - private void SetCurrentCollection( int idx ) + private void SetCurrentCollection( int idx, bool force ) { - if( idx == _currentCollectionIndex ) + if( !force && idx == _currentCollectionIndex ) { return; } - _manager.Collections.SetCurrentCollection( _collections[ idx + 1 ] ); + _manager.Collections.SetCurrentCollection( _collections[idx + 1] ); _currentCollectionIndex = idx; _selector.Cache.TriggerListReset(); if( _selector.Mod != null ) @@ -141,12 +158,12 @@ namespace Penumbra.UI } } - public void SetCurrentCollection( ModCollection collection ) + public void SetCurrentCollection( ModCollection collection, bool force = false ) { var idx = Array.IndexOf( _collections, collection ) - 1; if( idx >= 0 ) { - SetCurrentCollection( idx ); + SetCurrentCollection( idx, force ); } } @@ -159,7 +176,7 @@ namespace Penumbra.UI if( combo ) { - SetCurrentCollection( index ); + SetCurrentCollection( index, false ); } } @@ -205,18 +222,14 @@ namespace Penumbra.UI { ImGui.InputTextWithHint( "##New Character", "New Character Name", ref _newCharacterName, 32 ); - using var style = ImGuiRaii.PushStyle( ImGuiStyleVar.Alpha, 0.5f, _newCharacterName.Length == 0 ); - ImGui.SameLine(); - if( ImGui.Button( "Create New Character Collection" ) && _newCharacterName.Length > 0 ) + if( ImGuiCustom.DisableButton( "Create New Character Collection", _newCharacterName.Length > 0 )) { _manager.Collections.CreateCharacterCollection( _newCharacterName ); _currentCharacterIndices[ _newCharacterName ] = 0; _newCharacterName = string.Empty; } - style.Pop(); - ImGuiCustom.HoverTooltip( "A character collection will be used whenever you manually redraw a character with the Name you have set up.\n" + "If you enable automatic character redraws in the Settings tab, penumbra will try to use Character collections for corresponding characters automatically.\n" ); diff --git a/Penumbra/UI/MenuTabs/TabDebug.cs b/Penumbra/UI/MenuTabs/TabDebug.cs index 0b826ad5..924c1c30 100644 --- a/Penumbra/UI/MenuTabs/TabDebug.cs +++ b/Penumbra/UI/MenuTabs/TabDebug.cs @@ -342,6 +342,34 @@ namespace Penumbra.UI } } + private void DrawDebugTabMissingFiles() + { + if( !ImGui.CollapsingHeader( "Missing Files##Debug" ) ) + { + return; + } + + var manager = Service.Get(); + var cache = manager.Collections.CurrentCollection.Cache; + if( cache == null || !ImGui.BeginTable( "##MissingFilesDebugList", 1, ImGuiTableFlags.RowBg, -Vector2.UnitX)) + { + return; + } + + using var raii = ImGuiRaii.DeferredEnd( ImGui.EndTable ); + + foreach( var file in cache.MissingFiles ) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + if( ImGui.Selectable( file.FullName ) ) + { + ImGui.SetClipboardText( file.FullName ); + } + ImGuiCustom.HoverTooltip( "Click to copy to clipboard." ); + } + } + private void DrawDebugTab() { if( !ImGui.BeginTabItem( "Debug Tab" ) ) @@ -353,6 +381,8 @@ namespace Penumbra.UI DrawDebugTabGeneral(); ImGui.NewLine(); + DrawDebugTabMissingFiles(); + ImGui.NewLine(); DrawDebugTabRedraw(); ImGui.NewLine(); DrawDebugTabPlayers(); diff --git a/Penumbra/Util/ArrayExtensions.cs b/Penumbra/Util/ArrayExtensions.cs index 0429f446..8308890e 100644 --- a/Penumbra/Util/ArrayExtensions.cs +++ b/Penumbra/Util/ArrayExtensions.cs @@ -72,5 +72,16 @@ namespace Penumbra.Util array.Swap( idx1, idx2 ); } + + public static int IndexOf< T >( this IList< T > array, Func< T, bool > predicate ) + { + for( var i = 0; i < array.Count; ++i ) + { + if( predicate.Invoke( array[ i ] ) ) + return i; + } + + return -1; + } } } \ No newline at end of file