diff --git a/OtterGui b/OtterGui index 5968fc8d..05619f96 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 5968fc8dde7867ec9b7216deeed93d7b59a41ab8 +Subproject commit 05619f966e6acfe8b0b6e947243c5d930c7525a4 diff --git a/Penumbra/Api/ModsController.cs b/Penumbra/Api/ModsController.cs index 03cf6885..bde6d2db 100644 --- a/Penumbra/Api/ModsController.cs +++ b/Penumbra/Api/ModsController.cs @@ -16,15 +16,17 @@ public class ModsController : WebApiController [Route( HttpVerbs.Get, "/mods" )] public object? GetMods() { - return Penumbra.ModManager.Mods.Zip( Penumbra.CollectionManager.Current.ActualSettings ).Select( x => new - { - x.Second?.Enabled, - x.Second?.Priority, - x.First.BasePath.Name, - x.First.Meta, - BasePath = x.First.BasePath.FullName, - Files = x.First.Resources.ModFiles.Select( fi => fi.FullName ), - } ); + // TODO + return null; + //return Penumbra.ModManager.Mods.Zip( Penumbra.CollectionManager.Current.ActualSettings ).Select( x => new + //{ + // x.Second?.Enabled, + // x.Second?.Priority, + // x.First.BasePath.Name, + // x.First.Name, + // BasePath = x.First.BasePath.FullName, + // Files = x.First.Resources.ModFiles.Select( fi => fi.FullName ), + //} ); } [Route( HttpVerbs.Post, "/mods" )] diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index cac23ecb..8610ac17 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -76,7 +76,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi _penumbra!.ObjectReloader.RedrawAll( setting ); } - private static string ResolvePath( string path, Mods.Mod.Manager _, ModCollection collection ) + private static string ResolvePath( string path, Mods.Mod2.Manager _, ModCollection collection ) { if( !Penumbra.Config.EnableMods ) { diff --git a/Penumbra/Api/SimpleRedirectManager.cs b/Penumbra/Api/SimpleRedirectManager.cs new file mode 100644 index 00000000..1c633a65 --- /dev/null +++ b/Penumbra/Api/SimpleRedirectManager.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using Dalamud.Logging; +using Penumbra.GameData.ByteString; +using Penumbra.Mods; + +namespace Penumbra.Api; + +public enum RedirectResult +{ + Registered = 0, + Success = 0, + IdenticalFileRegistered = 1, + InvalidGamePath = 2, + OtherOwner = 3, + NotRegistered = 4, + NoPermission = 5, + FilteredGamePath = 6, + UnknownError = 7, +} + +public class SimpleRedirectManager +{ + internal readonly Dictionary< Utf8GamePath, (FullPath File, string Tag) > Replacements = new(); + public readonly HashSet< string > AllowedTags = new(); + + public void Apply( IDictionary< Utf8GamePath, FullPath > dict ) + { + foreach( var (gamePath, (file, _)) in Replacements ) + { + dict.TryAdd( gamePath, file ); + } + } + + private RedirectResult? CheckPermission( string tag ) + => AllowedTags.Contains( tag ) ? null : RedirectResult.NoPermission; + + public RedirectResult IsRegistered( Utf8GamePath path, string tag ) + => CheckPermission( tag ) + ?? ( Replacements.TryGetValue( path, out var pair ) + ? pair.Tag == tag ? RedirectResult.Registered : RedirectResult.OtherOwner + : RedirectResult.NotRegistered ); + + public RedirectResult Register( Utf8GamePath path, FullPath file, string tag ) + { + if( CheckPermission( tag ) != null ) + { + return RedirectResult.NoPermission; + } + + if( Mod2.FilterFile( path ) ) + { + return RedirectResult.FilteredGamePath; + } + + try + { + if( Replacements.TryGetValue( path, out var pair ) ) + { + if( file.Equals( pair.File ) ) + { + return RedirectResult.IdenticalFileRegistered; + } + + if( tag != pair.Tag ) + { + return RedirectResult.OtherOwner; + } + } + + Replacements[ path ] = ( file, tag ); + return RedirectResult.Success; + } + catch( Exception e ) + { + PluginLog.Error( $"[{tag}] Unknown Error registering simple redirect {path} -> {file}:\n{e}" ); + return RedirectResult.UnknownError; + } + } + + public RedirectResult Unregister( Utf8GamePath path, string tag ) + { + if( CheckPermission( tag ) != null ) + { + return RedirectResult.NoPermission; + } + + try + { + if( !Replacements.TryGetValue( path, out var pair ) ) + { + return RedirectResult.NotRegistered; + } + + if( tag != pair.Tag ) + { + return RedirectResult.OtherOwner; + } + + Replacements.Remove( path ); + return RedirectResult.Success; + } + catch( Exception e ) + { + PluginLog.Error( $"[{tag}] Unknown Error unregistering simple redirect {path}:\n{e}" ); + return RedirectResult.UnknownError; + } + } + + public RedirectResult Register( string path, string file, string tag ) + => Utf8GamePath.FromString( path, out var gamePath, true ) + ? Register( gamePath, new FullPath( file ), tag ) + : RedirectResult.InvalidGamePath; + + public RedirectResult Unregister( string path, string tag ) + => Utf8GamePath.FromString( path, out var gamePath, true ) + ? Unregister( gamePath, tag ) + : RedirectResult.InvalidGamePath; + + public RedirectResult IsRegistered( string path, string tag ) + => Utf8GamePath.FromString( path, out var gamePath, true ) + ? IsRegistered( gamePath, tag ) + : RedirectResult.InvalidGamePath; +} \ No newline at end of file diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index fe396ccd..8674d5a3 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -275,7 +275,7 @@ public partial class ModCollection } } - private void OnModRemovedActive( bool meta, IEnumerable< ModSettings? > settings ) + private void OnModRemovedActive( bool meta, IEnumerable< ModSettings2? > settings ) { foreach( var (collection, _) in this.Zip( settings ).Where( c => c.First.HasCache && c.Second?.Enabled == true ) ) { diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 752a5349..3a7335cc 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -27,7 +27,7 @@ public partial class ModCollection public delegate void CollectionChangeDelegate( Type type, ModCollection? oldCollection, ModCollection? newCollection, string? characterName = null ); - private readonly Mod.Manager _modManager; + private readonly Mod2.Manager _modManager; // The empty collection is always available and always has index 0. // It can not be deleted or moved. @@ -56,14 +56,15 @@ public partial class ModCollection IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public Manager( Mod.Manager manager ) + public Manager( Mod2.Manager manager ) { _modManager = manager; // The collection manager reacts to changes in mods by itself. _modManager.ModDiscoveryStarted += OnModDiscoveryStarted; _modManager.ModDiscoveryFinished += OnModDiscoveryFinished; - _modManager.ModChange += OnModChanged; + _modManager.ModOptionChanged += OnModOptionsChanged; + _modManager.ModPathChanged += OnModPathChanged; CollectionChanged += SaveOnChange; ReadCollections(); LoadCollections(); @@ -73,7 +74,8 @@ public partial class ModCollection { _modManager.ModDiscoveryStarted -= OnModDiscoveryStarted; _modManager.ModDiscoveryFinished -= OnModDiscoveryFinished; - _modManager.ModChange -= OnModChanged; + _modManager.ModOptionChanged -= OnModOptionsChanged; + _modManager.ModPathChanged -= OnModPathChanged; } // Add a new collection of the given name. @@ -171,42 +173,64 @@ public partial class ModCollection } - // A changed mod forces changes for all collections, active and inactive. - private void OnModChanged( Mod.ChangeType type, Mod mod ) + // A changed mod path forces changes for all collections, active and inactive. + private void OnModPathChanged( ModPathChangeType type, Mod2 mod, DirectoryInfo? oldDirectory, + DirectoryInfo? newDirectory ) { switch( type ) { - case Mod.ChangeType.Added: + case ModPathChangeType.Added: foreach( var collection in this ) { collection.AddMod( mod ); } - OnModAddedActive( mod.Resources.MetaManipulations.Count > 0 ); + OnModAddedActive( mod.TotalManipulations > 0 ); break; - case Mod.ChangeType.Removed: - var settings = new List< ModSettings? >( _collections.Count ); + case ModPathChangeType.Deleted: + var settings = new List< ModSettings2? >( _collections.Count ); foreach( var collection in this ) { settings.Add( collection[ mod.Index ].Settings ); collection.RemoveMod( mod, mod.Index ); } - OnModRemovedActive( mod.Resources.MetaManipulations.Count > 0, settings ); + OnModRemovedActive( mod.TotalManipulations > 0, settings ); break; - case Mod.ChangeType.Changed: - foreach( var collection in this.Where( - collection => collection.Settings[ mod.Index ]?.FixInvalidSettings( mod.Meta ) ?? false ) ) + case ModPathChangeType.Moved: + foreach( var collection in this.Where( collection => collection.Settings[ mod.Index ] != null ) ) { collection.Save(); } - OnModChangedActive( mod.Resources.MetaManipulations.Count > 0, mod.Index ); + OnModChangedActive( mod.TotalManipulations > 0, mod.Index ); break; default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); } } + + private void OnModOptionsChanged( ModOptionChangeType type, Mod2 mod, int groupIdx, int optionIdx ) + { + if( type == ModOptionChangeType.DisplayChange ) + { + return; + } + + // TODO + switch( type ) + { + case ModOptionChangeType.GroupRenamed: + case ModOptionChangeType.GroupAdded: + case ModOptionChangeType.GroupDeleted: + case ModOptionChangeType.PriorityChanged: + case ModOptionChangeType.OptionAdded: + case ModOptionChangeType.OptionDeleted: + case ModOptionChangeType.OptionChanged: + default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); + } + } + // Add the collection with the default name if it does not exist. // It should always be ensured that it exists, otherwise it will be created. // This can also not be deleted, so there are always at least the empty and a collection with default name. diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs new file mode 100644 index 00000000..8e58762a --- /dev/null +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Dalamud.Logging; +using Penumbra.GameData.ByteString; +using Penumbra.Meta.Manager; + +namespace Penumbra.Collections; + +public partial class ModCollection +{ + // Only active collections need to have a cache. + private Cache? _cache; + + public bool HasCache + => _cache != null; + + // Only create, do not update. + public void CreateCache( bool isDefault ) + { + if( _cache == null ) + { + CalculateEffectiveFileList( true, isDefault ); + } + } + + // Force an update with metadata for this cache. + public void ForceCacheUpdate( bool isDefault ) + => CalculateEffectiveFileList( true, isDefault ); + + + // Clear the current cache. + public void ClearCache() + { + _cache?.Dispose(); + _cache = null; + } + + + public FullPath? ResolvePath( Utf8GamePath path ) + => _cache?.ResolvePath( path ); + + // Force a file to be resolved to a specific path regardless of conflicts. + internal void ForceFile( Utf8GamePath path, FullPath fullPath ) + => _cache!.ResolvedFiles[ path ] = fullPath; + + // Force a file resolve to be removed. + internal void RemoveFile( Utf8GamePath path ) + => _cache!.ResolvedFiles.Remove( path ); + + // Obtain data from the cache. + internal MetaManager? MetaCache + => _cache?.MetaManipulations; + + internal IReadOnlyDictionary< Utf8GamePath, FullPath > ResolvedFiles + => _cache?.ResolvedFiles ?? new Dictionary< Utf8GamePath, FullPath >(); + + internal IReadOnlySet< FullPath > MissingFiles + => _cache?.MissingFiles ?? new HashSet< FullPath >(); + + internal IReadOnlyDictionary< string, object? > ChangedItems + => _cache?.ChangedItems ?? new Dictionary< string, object? >(); + + internal IReadOnlyList< ConflictCache.Conflict > Conflicts + => _cache?.Conflicts.Conflicts ?? Array.Empty< ConflictCache.Conflict >(); + + internal IEnumerable< ConflictCache.Conflict > ModConflicts( int modIdx ) + => _cache?.Conflicts.ModConflicts( modIdx ) ?? Array.Empty< ConflictCache.Conflict >(); + + // Update the effective file list for the given cache. + // Creates a cache if necessary. + public void CalculateEffectiveFileList( bool withMetaManipulations, bool reloadDefault ) + { + // Skip the empty collection. + if( Index == 0 ) + { + return; + } + + PluginLog.Debug( "Recalculating effective file list for {CollectionName} [{WithMetaManipulations}] [{ReloadDefault}]", Name, + withMetaManipulations, reloadDefault ); + _cache ??= new Cache( this ); + _cache.CalculateEffectiveFileList( withMetaManipulations ); + if( reloadDefault ) + { + SetFiles(); + Penumbra.ResidentResources.Reload(); + } + } + + // Set Metadata files. + [Conditional( "USE_EQP" )] + public void SetEqpFiles() + { + if( _cache == null ) + { + MetaManager.MetaManagerEqp.ResetFiles(); + } + else + { + _cache.MetaManipulations.Eqp.SetFiles(); + } + } + + [Conditional( "USE_EQDP" )] + public void SetEqdpFiles() + { + if( _cache == null ) + { + MetaManager.MetaManagerEqdp.ResetFiles(); + } + else + { + _cache.MetaManipulations.Eqdp.SetFiles(); + } + } + + [Conditional( "USE_GMP" )] + public void SetGmpFiles() + { + if( _cache == null ) + { + MetaManager.MetaManagerGmp.ResetFiles(); + } + else + { + _cache.MetaManipulations.Gmp.SetFiles(); + } + } + + [Conditional( "USE_EST" )] + public void SetEstFiles() + { + if( _cache == null ) + { + MetaManager.MetaManagerEst.ResetFiles(); + } + else + { + _cache.MetaManipulations.Est.SetFiles(); + } + } + + [Conditional( "USE_CMP" )] + public void SetCmpFiles() + { + if( _cache == null ) + { + MetaManager.MetaManagerCmp.ResetFiles(); + } + else + { + _cache.MetaManipulations.Cmp.SetFiles(); + } + } + + public void SetFiles() + { + if( _cache == null ) + { + Penumbra.CharacterUtility.ResetAll(); + } + else + { + _cache.MetaManipulations.SetFiles(); + } + } +} \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 3e700cb5..6de9072c 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -1,193 +1,24 @@ using System; -using System.Collections; using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; using System.Linq; using Dalamud.Logging; using Penumbra.GameData.ByteString; using Penumbra.Meta.Manager; +using Penumbra.Meta.Manipulations; using Penumbra.Mods; -using Penumbra.Util; namespace Penumbra.Collections; public partial class ModCollection { - // Only active collections need to have a cache. - private Cache? _cache; - - public bool HasCache - => _cache != null; - - // Only create, do not update. - public void CreateCache( bool isDefault ) - { - if( _cache == null ) - { - CalculateEffectiveFileList( true, isDefault ); - } - } - - // Force an update with metadata for this cache. - public void ForceCacheUpdate( bool isDefault ) - => CalculateEffectiveFileList( true, isDefault ); - - - // Clear the current cache. - public void ClearCache() - { - _cache?.Dispose(); - _cache = null; - } - - - public FullPath? ResolvePath( Utf8GamePath path ) - => _cache?.ResolvePath( path ); - - // Force a file to be resolved to a specific path regardless of conflicts. - internal void ForceFile( Utf8GamePath path, FullPath fullPath ) - => _cache!.ResolvedFiles[ path ] = fullPath; - - // Force a file resolve to be removed. - internal void RemoveFile( Utf8GamePath path ) - => _cache!.ResolvedFiles.Remove( path ); - - // Obtain data from the cache. - internal MetaManager? MetaCache - => _cache?.MetaManipulations; - - internal IReadOnlyDictionary< Utf8GamePath, FullPath > ResolvedFiles - => _cache?.ResolvedFiles ?? new Dictionary< Utf8GamePath, FullPath >(); - - internal IReadOnlySet< FullPath > MissingFiles - => _cache?.MissingFiles ?? new HashSet< FullPath >(); - - internal IReadOnlyDictionary< string, object? > ChangedItems - => _cache?.ChangedItems ?? new Dictionary< string, object? >(); - - internal IReadOnlyList< ConflictCache.Conflict > Conflicts - => _cache?.Conflicts.Conflicts ?? Array.Empty< ConflictCache.Conflict >(); - - internal IEnumerable< ConflictCache.Conflict > ModConflicts( int modIdx ) - => _cache?.Conflicts.ModConflicts( modIdx ) ?? Array.Empty< ConflictCache.Conflict >(); - - // Update the effective file list for the given cache. - // Creates a cache if necessary. - public void CalculateEffectiveFileList( bool withMetaManipulations, bool reloadDefault ) - { - // Skip the empty collection. - if( Index == 0 ) - { - return; - } - - PluginLog.Debug( "Recalculating effective file list for {CollectionName} [{WithMetaManipulations}] [{ReloadDefault}]", Name, - withMetaManipulations, reloadDefault ); - _cache ??= new Cache( this ); - _cache.CalculateEffectiveFileList(); - if( withMetaManipulations ) - { - _cache.UpdateMetaManipulations(); - if( reloadDefault ) - { - SetFiles(); - } - } - - if( reloadDefault ) - { - Penumbra.ResidentResources.Reload(); - } - } - - // Set Metadata files. - [Conditional( "USE_EQP" )] - public void SetEqpFiles() - { - if( _cache == null ) - { - MetaManager.MetaManagerEqp.ResetFiles(); - } - else - { - _cache.MetaManipulations.Eqp.SetFiles(); - } - } - - [Conditional( "USE_EQDP" )] - public void SetEqdpFiles() - { - if( _cache == null ) - { - MetaManager.MetaManagerEqdp.ResetFiles(); - } - else - { - _cache.MetaManipulations.Eqdp.SetFiles(); - } - } - - [Conditional( "USE_GMP" )] - public void SetGmpFiles() - { - if( _cache == null ) - { - MetaManager.MetaManagerGmp.ResetFiles(); - } - else - { - _cache.MetaManipulations.Gmp.SetFiles(); - } - } - - [Conditional( "USE_EST" )] - public void SetEstFiles() - { - if( _cache == null ) - { - MetaManager.MetaManagerEst.ResetFiles(); - } - else - { - _cache.MetaManipulations.Est.SetFiles(); - } - } - - [Conditional( "USE_CMP" )] - public void SetCmpFiles() - { - if( _cache == null ) - { - MetaManager.MetaManagerCmp.ResetFiles(); - } - else - { - _cache.MetaManipulations.Cmp.SetFiles(); - } - } - - public void SetFiles() - { - if( _cache == null ) - { - Penumbra.CharacterUtility.ResetAll(); - } - else - { - _cache.MetaManipulations.SetFiles(); - } - } - - - // The ModCollectionCache contains all required temporary data to use a collection. + // 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 { // Shared caches to avoid allocations. - private static readonly BitArray FileSeen = new(256); - private static readonly Dictionary< Utf8GamePath, int > RegisteredFiles = new(256); - private static readonly List< ModSettings? > ResolvedSettings = new(128); + private static readonly Dictionary< Utf8GamePath, FileRegister > RegisteredFiles = new(1024); + private static readonly Dictionary< MetaManipulation, FileRegister > RegisteredManipulations = new(1024); + private static readonly List< ModSettings2? > ResolvedSettings = new(128); private readonly ModCollection _collection; private readonly SortedList< string, object? > _changedItems = new(); @@ -221,7 +52,24 @@ public partial class ModCollection _collection.InheritanceChanged -= OnInheritanceChange; } - private void OnModSettingChange( ModSettingChange type, int modIdx, int oldValue, string? optionName, bool _ ) + // 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.InternalName.Length > Utf8GamePath.MaxGamePathLength + || candidate.IsRooted && !candidate.Exists ) + { + return null; + } + + return candidate; + } + + 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. @@ -232,7 +80,7 @@ public partial class ModCollection } var hasMeta = type is ModSettingChange.MultiEnableState or ModSettingChange.MultiInheritance - || Penumbra.ModManager[ modIdx ].Resources.MetaManipulations.Count > 0; + || Penumbra.ModManager[ modIdx ].AllManipulations.Any(); _collection.CalculateEffectiveFileList( hasMeta, Penumbra.CollectionManager.Default == _collection ); } @@ -241,22 +89,6 @@ public partial class ModCollection private void OnInheritanceChange( bool _ ) => _collection.CalculateEffectiveFileList( true, true ); - // Reset the shared file-seen cache. - private static void ResetFileSeen( int size ) - { - if( size < FileSeen.Length ) - { - FileSeen.Length = size; - FileSeen.SetAll( false ); - } - else - { - FileSeen.SetAll( false ); - FileSeen.Length = size; - } - } - - // Clear all local and global caches to prepare for recomputation. private void ClearStorageAndPrepare() { @@ -270,21 +102,27 @@ public partial class ModCollection ResolvedSettings.AddRange( _collection.ActualSettings ); } - public void CalculateEffectiveFileList() + // 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.Clear(); + MetaManipulations.Reset(); + } + + AddCustomRedirects(); for( var i = 0; i < Penumbra.ModManager.Count; ++i ) { - if( ResolvedSettings[ i ]?.Enabled == true ) - { - AddFiles( i ); - AddSwaps( i ); - } + AddMod( i, withManipulations ); } AddMetaFiles(); } + // Identify and record all manipulated objects for this entire collection. private void SetChangedItems() { if( _changedItems.Count > 0 || ResolvedFiles.Count + MetaManipulations.Count == 0 ) @@ -309,228 +147,185 @@ public partial class ModCollection } } - private void AddFiles( int idx ) + // 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 ) { - var mod = Penumbra.ModManager.Mods[ idx ]; - ResetFileSeen( mod.Resources.ModFiles.Count ); - // Iterate in reverse so that later groups take precedence before earlier ones. - // TODO: add group priorities. - foreach( var group in mod.Meta.Groups.Values.Reverse() ) + if( RegisteredFiles.TryGetValue( path, out var register ) ) { - switch( group.SelectionType ) + if( register.SameMod( priority, out var less ) ) { - case SelectType.Single: - AddFilesForSingle( group, mod, idx ); - break; - case SelectType.Multi: - AddFilesForMulti( group, mod, idx ); - break; - default: throw new InvalidEnumArgumentException(); - } - } - - AddRemainingFiles( mod, idx ); - } - - // If audio streaming is not disabled, replacing .scd files crashes the game, - // so only add those files if it is disabled. - private static bool FilterFile( Utf8GamePath gamePath ) - => !Penumbra.Config.DisableSoundStreaming - && gamePath.Path.EndsWith( '.', 's', 'c', 'd' ); - - private void AddFile( int modIdx, Utf8GamePath gamePath, FullPath file ) - { - if( FilterFile( gamePath ) ) - { - return; - } - - if( !RegisteredFiles.TryGetValue( gamePath, out var oldModIdx ) ) - { - // No current conflict, just add. - RegisteredFiles.Add( gamePath, modIdx ); - ResolvedFiles[ gamePath ] = file; - } - else - { - // Conflict, check which mod has higher priority, replace if necessary, add conflict. - var priority = ResolvedSettings[ modIdx ]!.Priority; - var oldPriority = ResolvedSettings[ oldModIdx ]!.Priority; - Conflicts.AddConflict( oldModIdx, modIdx, oldPriority, priority, gamePath ); - if( priority > oldPriority ) - { - ResolvedFiles[ gamePath ] = file; - RegisteredFiles[ gamePath ] = modIdx; - } - } - } - - private void AddMissingFile( FullPath file ) - { - switch( file.Extension.ToLowerInvariant() ) - { - // We do not care for those file types - case ".scp" when !Penumbra.Config.DisableSoundStreaming: - case ".meta": - case ".rgsp": - return; - default: - MissingFiles.Add( file ); - return; - } - } - - private void AddPathsForOption( Option option, Mod mod, int modIdx, bool enabled ) - { - foreach( var (file, paths) in option.OptionFiles ) - { - // TODO: complete rework of options. - var fullPath = new FullPath( mod.BasePath, file ); - var idx = mod.Resources.ModFiles.IndexOf( f => f.Equals( fullPath ) ); - if( idx < 0 ) - { - AddMissingFile( fullPath ); - continue; - } - - var registeredFile = mod.Resources.ModFiles[ idx ]; - if( !registeredFile.Exists ) - { - AddMissingFile( registeredFile ); - continue; - } - - FileSeen.Set( idx, true ); - if( enabled ) - { - foreach( var path in paths ) + Conflicts.AddConflict( register.ModIdx, priority.ModIdx, register.ModPriority, priority.ModPriority, path ); + if( less ) { - AddFile( modIdx, path, registeredFile ); - } - } - } - } - - private void AddFilesForSingle( OptionGroup singleGroup, Mod mod, int modIdx ) - { - Debug.Assert( singleGroup.SelectionType == SelectType.Single ); - var settings = ResolvedSettings[ modIdx ]!; - if( !settings.Settings.TryGetValue( singleGroup.GroupName, out var setting ) ) - { - setting = 0; - } - - for( var i = 0; i < singleGroup.Options.Count; ++i ) - { - AddPathsForOption( singleGroup.Options[ i ], mod, modIdx, setting == i ); - } - } - - private void AddFilesForMulti( OptionGroup multiGroup, Mod mod, int modIdx ) - { - Debug.Assert( multiGroup.SelectionType == SelectType.Multi ); - var settings = ResolvedSettings[ modIdx ]!; - if( !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, modIdx, ( setting & ( 1 << i ) ) != 0 ); - } - } - - private void AddRemainingFiles( Mod mod, int modIdx ) - { - for( var i = 0; i < mod.Resources.ModFiles.Count; ++i ) - { - if( FileSeen.Get( i ) ) - { - continue; - } - - var file = mod.Resources.ModFiles[ i ]; - if( file.Exists ) - { - if( file.ToGamePath( mod.BasePath, out var gamePath ) ) - { - AddFile( modIdx, gamePath, file ); - } - else - { - PluginLog.Warning( $"Could not convert {file} in {mod.BasePath.FullName} to GamePath." ); + RegisteredFiles[ path ] = priority; + ResolvedFiles[ path ] = file; } } else { - MissingFiles.Add( file ); + // 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( Mod2.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 = ResolvedSettings[ modIdx ]; + if( settings is not { Enabled: true } ) + { + return; + } + + var mod = Penumbra.ModManager.Mods[ 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 ]; + 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(); - private void AddSwaps( int modIdx ) + // Add all API redirects. + private void AddCustomRedirects() { - var mod = Penumbra.ModManager.Mods[ modIdx ]; - foreach( var (gamePath, swapPath) in mod.Meta.FileSwaps.Where( kvp => !FilterFile( kvp.Key ) ) ) + Penumbra.Redirects.Apply( ResolvedFiles ); + foreach( var gamePath in ResolvedFiles.Keys ) { - AddFile( modIdx, gamePath, swapPath ); + RegisteredFiles.Add( gamePath, new FileRegister( -1, int.MaxValue, 0, 0 ) ); } } - private void AddManipulations( int modIdx ) + + // 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 ) { - var mod = Penumbra.ModManager.Mods[ modIdx ]; - foreach( var manip in mod.Resources.MetaManipulations.GetManipulationsForConfig( ResolvedSettings[ modIdx ]!, mod.Meta ) ) + public readonly int ModIdx = ModIdx; + public readonly int ModPriority = ModPriority; + public readonly int GroupPriority = GroupPriority; + public readonly int OptionPriority = OptionPriority; + + public bool SameMod( FileRegister other, out bool less ) { - if( !MetaManipulations.TryGetValue( manip, out var oldModIdx ) ) + if( ModIdx != other.ModIdx ) { - MetaManipulations.ApplyMod( manip, modIdx ); + less = ModPriority < other.ModPriority; + return true; + } + + if( GroupPriority < other.GroupPriority ) + { + less = true; + } + else if( GroupPriority == other.GroupPriority ) + { + less = OptionPriority < other.OptionPriority; } else { - var priority = ResolvedSettings[ modIdx ]!.Priority; - var oldPriority = ResolvedSettings[ oldModIdx ]!.Priority; - Conflicts.AddConflict( oldModIdx, modIdx, oldPriority, priority, manip ); - if( priority > oldPriority ) - { - MetaManipulations.ApplyMod( manip, modIdx ); - } + less = false; } + + return false; } - } - - public void UpdateMetaManipulations() - { - MetaManipulations.Reset(); - Conflicts.ClearMetaConflicts(); - - foreach( var mod in Penumbra.ModManager.Mods.Zip( ResolvedSettings ) - .Select( ( m, i ) => ( m.First, m.Second, i ) ) - .Where( m => m.Second?.Enabled == true && m.First.Resources.MetaManipulations.Count > 0 ) ) - { - AddManipulations( mod.i ); - } - } - - public FullPath? ResolvePath( Utf8GamePath gameResourcePath ) - { - if( !ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ) ) - { - return null; - } - - if( candidate.InternalName.Length > Utf8GamePath.MaxGamePathLength - || candidate.IsRooted && !candidate.Exists ) - { - return null; - } - - return candidate; - } + }; } } \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.Changes.cs b/Penumbra/Collections/ModCollection.Changes.cs index 114efe63..78e6ee07 100644 --- a/Penumbra/Collections/ModCollection.Changes.cs +++ b/Penumbra/Collections/ModCollection.Changes.cs @@ -20,7 +20,7 @@ public partial class ModCollection { // If the change type is a bool, oldValue will be 1 for true and 0 for false. // optionName will only be set for type == Setting. - public delegate void ModSettingChangeDelegate( ModSettingChange type, int modIdx, int oldValue, string? optionName, bool inherited ); + public delegate void ModSettingChangeDelegate( ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool inherited ); public event ModSettingChangeDelegate ModSettingChanged; // Enable or disable the mod inheritance of mod idx. @@ -28,7 +28,7 @@ public partial class ModCollection { if( FixInheritance( idx, inherit ) ) { - ModSettingChanged.Invoke( ModSettingChange.Inheritance, idx, inherit ? 0 : 1, null, false ); + ModSettingChanged.Invoke( ModSettingChange.Inheritance, idx, inherit ? 0 : 1, 0, false ); } } @@ -41,22 +41,22 @@ public partial class ModCollection { var inheritance = FixInheritance( idx, false ); _settings[ idx ]!.Enabled = newValue; - ModSettingChanged.Invoke( ModSettingChange.EnableState, idx, inheritance ? -1 : newValue ? 0 : 1, null, false ); + ModSettingChanged.Invoke( ModSettingChange.EnableState, idx, inheritance ? -1 : newValue ? 0 : 1, 0, false ); } } // Enable or disable the mod inheritance of every mod in mods. - public void SetMultipleModInheritances( IEnumerable< Mod > mods, bool inherit ) + public void SetMultipleModInheritances( IEnumerable< Mod2 > mods, bool inherit ) { if( mods.Aggregate( false, ( current, mod ) => current | FixInheritance( mod.Index, inherit ) ) ) { - ModSettingChanged.Invoke( ModSettingChange.MultiInheritance, -1, -1, null, false ); + ModSettingChanged.Invoke( ModSettingChange.MultiInheritance, -1, -1, 0, false ); } } // Set the enabled state of every mod in mods to the new value. // If the mod is currently inherited, stop the inheritance. - public void SetMultipleModStates( IEnumerable< Mod > mods, bool newValue ) + public void SetMultipleModStates( IEnumerable< Mod2 > mods, bool newValue ) { var changes = false; foreach( var mod in mods ) @@ -72,7 +72,7 @@ public partial class ModCollection if( changes ) { - ModSettingChanged.Invoke( ModSettingChange.MultiEnableState, -1, -1, null, false ); + ModSettingChanged.Invoke( ModSettingChange.MultiEnableState, -1, -1, 0, false ); } } @@ -85,26 +85,21 @@ public partial class ModCollection { var inheritance = FixInheritance( idx, false ); _settings[ idx ]!.Priority = newValue; - ModSettingChanged.Invoke( ModSettingChange.Priority, idx, inheritance ? -1 : oldValue, null, false ); + ModSettingChanged.Invoke( ModSettingChange.Priority, idx, inheritance ? -1 : oldValue, 0, false ); } } // Set a given setting group settingName of mod idx to newValue if it differs from the current value and fix it if necessary. // If mod idx is currently inherited, stop the inheritance. - public void SetModSetting( int idx, string settingName, int newValue ) + public void SetModSetting( int idx, int groupIdx, uint newValue ) { var settings = _settings[ idx ] != null ? _settings[ idx ]!.Settings : this[ idx ].Settings?.Settings; - var oldValue = settings != null - ? settings.TryGetValue( settingName, out var v ) ? v : newValue - : Penumbra.ModManager.Mods[ idx ].Meta.Groups.ContainsKey( settingName ) - ? 0 - : newValue; + var oldValue = settings?[ groupIdx ] ?? 0; if( oldValue != newValue ) { var inheritance = FixInheritance( idx, false ); - _settings[ idx ]!.Settings[ settingName ] = newValue; - _settings[ idx ]!.FixSpecificSetting( settingName, Penumbra.ModManager.Mods[ idx ].Meta ); - ModSettingChanged.Invoke( ModSettingChange.Setting, idx, inheritance ? -1 : oldValue, settingName, false ); + _settings[ idx ]!.SetValue( Penumbra.ModManager.Mods[ idx ], groupIdx, newValue ); + ModSettingChanged.Invoke( ModSettingChange.Setting, idx, inheritance ? -1 : ( int )oldValue, groupIdx, false ); } } @@ -112,7 +107,7 @@ public partial class ModCollection // If type == Setting, settingName should be a valid setting for that mod, otherwise it will be ignored. // The setting will also be automatically fixed if it is invalid for that setting group. // For boolean parameters, newValue == 0 will be treated as false and != 0 as true. - public void ChangeModSetting( ModSettingChange type, int idx, int newValue, string? settingName = null ) + public void ChangeModSetting( ModSettingChange type, int idx, int newValue, int groupIdx ) { switch( type ) { @@ -126,7 +121,7 @@ public partial class ModCollection SetModPriority( idx, newValue ); break; case ModSettingChange.Setting: - SetModSetting( idx, settingName ?? string.Empty, newValue ); + SetModSetting( idx, groupIdx, ( uint )newValue ); break; default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); } @@ -142,11 +137,11 @@ public partial class ModCollection return false; } - _settings[ idx ] = inherit ? null : this[ idx ].Settings ?? ModSettings.DefaultSettings( Penumbra.ModManager.Mods[ idx ].Meta ); + _settings[ idx ] = inherit ? null : this[ idx ].Settings ?? ModSettings2.DefaultSettings( Penumbra.ModManager.Mods[ idx ] ); return true; } - private void SaveOnChange( ModSettingChange _1, int _2, int _3, string? _4, bool inherited ) + private void SaveOnChange( ModSettingChange _1, int _2, int _3, int _4, bool inherited ) => SaveOnChange( inherited ); private void SaveOnChange( bool inherited ) diff --git a/Penumbra/Collections/ModCollection.File.cs b/Penumbra/Collections/ModCollection.File.cs index cad2c209..0311e0b0 100644 --- a/Penumbra/Collections/ModCollection.File.cs +++ b/Penumbra/Collections/ModCollection.File.cs @@ -48,7 +48,7 @@ public partial class ModCollection if( settings != null ) { j.WritePropertyName( Penumbra.ModManager[ i ].BasePath.Name ); - x.Serialize( j, settings ); + x.Serialize( j, new ModSettings2.SavedSettings( settings, Penumbra.ModManager[ i ] ) ); } } @@ -111,8 +111,8 @@ public partial class ModCollection var name = obj[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty; var version = obj[ nameof( Version ) ]?.ToObject< int >() ?? 0; // Custom deserialization that is converted with the constructor. - var settings = obj[ nameof( Settings ) ]?.ToObject< Dictionary< string, ModSettings > >() - ?? new Dictionary< string, ModSettings >(); + var settings = obj[ nameof( Settings ) ]?.ToObject< Dictionary< string, ModSettings2.SavedSettings > >() + ?? new Dictionary< string, ModSettings2.SavedSettings >(); inheritance = obj[ nameof( Inheritance ) ]?.ToObject< List< string > >() ?? ( IReadOnlyList< string > )Array.Empty< string >(); return new ModCollection( name, version, settings ); diff --git a/Penumbra/Collections/ModCollection.Inheritance.cs b/Penumbra/Collections/ModCollection.Inheritance.cs index e333e6e0..34156396 100644 --- a/Penumbra/Collections/ModCollection.Inheritance.cs +++ b/Penumbra/Collections/ModCollection.Inheritance.cs @@ -71,11 +71,11 @@ public partial class ModCollection } // Carry changes in collections inherited from forward if they are relevant for this collection. - private void OnInheritedModSettingChange( ModSettingChange type, int modIdx, int oldValue, string? optionName, bool _ ) + private void OnInheritedModSettingChange( ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool _ ) { if( _settings[ modIdx ] == null ) { - ModSettingChanged.Invoke( type, modIdx, oldValue, optionName, true ); + ModSettingChanged.Invoke( type, modIdx, oldValue, groupIdx, true ); } } @@ -85,7 +85,7 @@ public partial class ModCollection // Obtain the actual settings for a given mod via index. // Also returns the collection the settings are taken from. // If no collection provides settings for this mod, this collection is returned together with null. - public (ModSettings? Settings, ModCollection Collection) this[ Index idx ] + public (ModSettings2? Settings, ModCollection Collection) this[ Index idx ] { get { diff --git a/Penumbra/Collections/ModCollection.Migration.cs b/Penumbra/Collections/ModCollection.Migration.cs index 42903ea8..4bfcccda 100644 --- a/Penumbra/Collections/ModCollection.Migration.cs +++ b/Penumbra/Collections/ModCollection.Migration.cs @@ -45,10 +45,13 @@ public sealed partial class ModCollection } // We treat every completely defaulted setting as inheritance-ready. - private static bool SettingIsDefaultV0( ModSettings? setting ) + private static bool SettingIsDefaultV0( ModSettings2.SavedSettings setting ) => setting is { Enabled: false, Priority: 0 } && setting.Settings.Values.All( s => s == 0 ); + + private static bool SettingIsDefaultV0( ModSettings2? setting ) + => setting is { Enabled: false, Priority: 0 } && setting.Settings.All( s => s == 0 ); } - internal static ModCollection MigrateFromV0( string name, Dictionary< string, ModSettings > allSettings ) + internal static ModCollection MigrateFromV0( string name, Dictionary< string, ModSettings2.SavedSettings > allSettings ) => new(name, 0, allSettings); } \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 6e82617f..ce3df492 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -27,17 +27,17 @@ public partial class ModCollection // If a ModSetting is null, it can be inherited from other collections. // If no collection provides a setting for the mod, it is just disabled. - private readonly List< ModSettings? > _settings; + private readonly List< ModSettings2? > _settings; - public IReadOnlyList< ModSettings? > Settings + public IReadOnlyList< ModSettings2? > Settings => _settings; // Evaluates the settings along the whole inheritance tree. - public IEnumerable< ModSettings? > ActualSettings + public IEnumerable< ModSettings2? > ActualSettings => Enumerable.Range( 0, _settings.Count ).Select( i => this[ i ].Settings ); // Settings for deleted mods will be kept via directory name. - private readonly Dictionary< string, ModSettings > _unusedSettings; + private readonly Dictionary< string, ModSettings2.SavedSettings > _unusedSettings; // Constructor for duplication. private ModCollection( string name, ModCollection duplicate ) @@ -52,13 +52,13 @@ public partial class ModCollection } // Constructor for reading from files. - private ModCollection( string name, int version, Dictionary< string, ModSettings > allSettings ) + private ModCollection( string name, int version, Dictionary< string, ModSettings2.SavedSettings > allSettings ) { Name = name; Version = version; _unusedSettings = allSettings; - _settings = new List< ModSettings? >(); + _settings = new List< ModSettings2? >(); ApplyModSettings(); Migration.Migrate( this ); @@ -68,7 +68,7 @@ public partial class ModCollection // Create a new, unique empty collection of a given name. public static ModCollection CreateNewEmpty( string name ) - => new(name, CurrentVersion, new Dictionary< string, ModSettings >()); + => new(name, CurrentVersion, new Dictionary< string, ModSettings2.SavedSettings >()); // Duplicate the calling collection to a new, unique collection of a given name. public ModCollection Duplicate( string name ) @@ -86,26 +86,27 @@ public partial class ModCollection } // Add settings for a new appended mod, by checking if the mod had settings from a previous deletion. - private void AddMod( Mod mod ) + private bool AddMod( Mod2 mod ) { - if( _unusedSettings.TryGetValue( mod.BasePath.Name, out var settings ) ) + if( _unusedSettings.TryGetValue( mod.BasePath.Name, out var save ) ) { + var ret = save.ToSettings( mod, out var settings ); _settings.Add( settings ); _unusedSettings.Remove( mod.BasePath.Name ); + return ret; } - else - { - _settings.Add( null ); - } + + _settings.Add( null ); + return false; } // Move settings from the current mod list to the unused mod settings. - private void RemoveMod( Mod mod, int idx ) + private void RemoveMod( Mod2 mod, int idx ) { var settings = _settings[ idx ]; if( settings != null ) { - _unusedSettings.Add( mod.BasePath.Name, settings ); + _unusedSettings.Add( mod.BasePath.Name, new ModSettings2.SavedSettings( settings, mod ) ); } _settings.RemoveAt( idx ); @@ -126,7 +127,7 @@ public partial class ModCollection { foreach( var (mod, setting) in Penumbra.ModManager.Zip( _settings ).Where( s => s.Second != null ) ) { - _unusedSettings[ mod.BasePath.Name ] = setting!; + _unusedSettings[ mod.BasePath.Name ] = new ModSettings2.SavedSettings( setting!, mod ); } _settings.Clear(); @@ -137,22 +138,7 @@ public partial class ModCollection private void ApplyModSettings() { _settings.Capacity = Math.Max( _settings.Capacity, Penumbra.ModManager.Count ); - var changes = false; - foreach( var mod in Penumbra.ModManager ) - { - if( _unusedSettings.TryGetValue( mod.BasePath.Name, out var s ) ) - { - changes |= s.FixInvalidSettings( mod.Meta ); - _settings.Add( s ); - _unusedSettings.Remove( mod.BasePath.Name ); - } - else - { - _settings.Add( null ); - } - } - - if( changes ) + if( Penumbra.ModManager.Aggregate( false, ( current, mod ) => current | AddMod( mod ) ) ) { Save(); } diff --git a/Penumbra/Importer/TexToolsImport.cs b/Penumbra/Importer/TexToolsImport.cs index a816d6a5..0caa5a05 100644 --- a/Penumbra/Importer/TexToolsImport.cs +++ b/Penumbra/Importer/TexToolsImport.cs @@ -6,7 +6,6 @@ using System.Text; using Dalamud.Logging; using ICSharpCode.SharpZipLib.Zip; using Newtonsoft.Json; -using Penumbra.GameData.ByteString; using Penumbra.Importer.Models; using Penumbra.Mods; using Penumbra.Util; @@ -148,23 +147,13 @@ internal class TexToolsImport var modList = modListRaw.Select( JsonConvert.DeserializeObject< SimpleMod > ); - // Create a new ModMeta from the TTMP modlist info - var modMeta = new ModMeta - { - Author = "Unknown", - Name = modPackFile.Name, - Description = "Mod imported from TexTools mod pack", - }; - // Open the mod data file from the modpack as a SqPackStream using var modData = GetMagicSqPackDeleterStream( extractedModPack, "TTMPD.mpd" ); ExtractedDirectory = CreateModFolder( _outDirectory, Path.GetFileNameWithoutExtension( modPackFile.Name ) ); - - File.WriteAllText( - Path.Combine( ExtractedDirectory.FullName, "meta.json" ), - JsonConvert.SerializeObject( modMeta ) - ); + // Create a new ModMeta from the TTMP modlist info + Mod2.CreateMeta( ExtractedDirectory, string.IsNullOrEmpty( modPackFile.Name ) ? "New Mod" : modPackFile.Name, "Unknown", + "Mod imported from TexTools mod pack.", null, null ); ExtractSimpleModList( ExtractedDirectory, modList, modData ); @@ -173,7 +162,7 @@ internal class TexToolsImport private DirectoryInfo ImportV2ModPack( FileInfo _, ZipFile extractedModPack, string modRaw ) { - var modList = JsonConvert.DeserializeObject( modRaw ); + var modList = JsonConvert.DeserializeObject< SimpleModPack >( modRaw ); if( modList.TTMPVersion?.EndsWith( "s" ) ?? false ) { @@ -233,23 +222,13 @@ internal class TexToolsImport { PluginLog.Log( " -> Importing Simple V2 ModPack" ); - // Create a new ModMeta from the TTMP modlist info - var modMeta = new ModMeta - { - Author = modList.Author ?? "Unknown", - Name = modList.Name ?? "New Mod", - Description = string.IsNullOrEmpty( modList.Description ) - ? "Mod imported from TexTools mod pack" - : modList.Description!, - }; - // Open the mod data file from the modpack as a SqPackStream using var modData = GetMagicSqPackDeleterStream( extractedModPack, "TTMPD.mpd" ); ExtractedDirectory = CreateModFolder( _outDirectory, modList.Name ?? "New Mod" ); - - File.WriteAllText( Path.Combine( ExtractedDirectory.FullName, "meta.json" ), - JsonConvert.SerializeObject( modMeta ) ); + Mod2.CreateMeta( ExtractedDirectory, modList.Name ?? "New Mod", modList.Author ?? "Unknown", string.IsNullOrEmpty( modList.Description ) + ? "Mod imported from TexTools mod pack" + : modList.Description, null, null ); ExtractSimpleModList( ExtractedDirectory, modList.SimpleModsList ?? Enumerable.Empty< SimpleMod >(), modData ); return ExtractedDirectory; @@ -261,21 +240,12 @@ internal class TexToolsImport var modList = JsonConvert.DeserializeObject< ExtendedModPack >( modRaw ); - // Create a new ModMeta from the TTMP modlist info - var modMeta = new ModMeta - { - Author = modList.Author ?? "Unknown", - Name = modList.Name ?? "New Mod", - Description = string.IsNullOrEmpty( modList.Description ) - ? "Mod imported from TexTools mod pack" - : modList.Description ?? "", - Version = modList.Version ?? "", - }; - // Open the mod data file from the modpack as a SqPackStream using var modData = GetMagicSqPackDeleterStream( extractedModPack, "TTMPD.mpd" ); ExtractedDirectory = CreateModFolder( _outDirectory, modList.Name ?? "New Mod" ); + Mod2.CreateMeta( ExtractedDirectory, modList.Name ?? "New Mod", modList.Author ?? "Unknown", + string.IsNullOrEmpty( modList.Description ) ? "Mod imported from TexTools mod pack" : modList.Description, modList.Version, null ); if( modList.SimpleModsList != null ) { @@ -288,6 +258,8 @@ internal class TexToolsImport } // Iterate through all pages + var options = new List< ISubMod >(); + var groupPriority = 0; foreach( var page in modList.ModPackPages ) { if( page.ModGroups == null ) @@ -297,6 +269,8 @@ internal class TexToolsImport foreach( var group in page.ModGroups.Where( group => group.GroupName != null && group.OptionList != null ) ) { + options.Clear(); + var description = new StringBuilder(); var groupFolder = NewOptionDirectory( ExtractedDirectory, group.GroupName! ); if( groupFolder.Exists ) { @@ -308,52 +282,19 @@ internal class TexToolsImport { var optionFolder = NewOptionDirectory( groupFolder, option.Name! ); ExtractSimpleModList( optionFolder, option.ModsJsons!, modData ); - } - - AddMeta( ExtractedDirectory, groupFolder, group, modMeta ); - } - } - - File.WriteAllText( - Path.Combine( ExtractedDirectory.FullName, "meta.json" ), - JsonConvert.SerializeObject( modMeta, Formatting.Indented ) - ); - return ExtractedDirectory; - } - - private static void AddMeta( DirectoryInfo baseFolder, DirectoryInfo groupFolder, ModGroup group, ModMeta meta ) - { - var inf = new OptionGroup - { - SelectionType = group.SelectionType, - GroupName = group.GroupName!, - Options = new List< Option >(), - }; - foreach( var opt in group.OptionList! ) - { - var option = new Option - { - OptionName = opt.Name!, - OptionDesc = string.IsNullOrEmpty( opt.Description ) ? "" : opt.Description!, - OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(), - }; - var optDir = NewOptionDirectory( groupFolder, opt.Name! ); - if( optDir.Exists ) - { - foreach( var file in optDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) - { - if( Utf8RelPath.FromFile( file, baseFolder, out var rel ) - && Utf8GamePath.FromFile( file, optDir, out var game, true ) ) + options.Add( Mod2.CreateSubMod( ExtractedDirectory, optionFolder, option ) ); + description.Append( option.Description ); + if( !string.IsNullOrEmpty( option.Description ) ) { - option.AddFile( rel, game ); + description.Append( '\n' ); } } + + Mod2.CreateOptionGroup( ExtractedDirectory, group, groupPriority++, description.ToString(), options ); } - - inf.Options.Add( option ); } - - meta.Groups.Add( inf.GroupName, inf ); + Mod2.CreateDefaultFiles( ExtractedDirectory ); + return ExtractedDirectory; } private void ImportMetaModPack( FileInfo file ) diff --git a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs index f7e78b12..dd7eb89e 100644 --- a/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs +++ b/Penumbra/Interop/Loader/ResourceLoader.Replacement.cs @@ -92,7 +92,7 @@ public unsafe partial class ResourceLoader // Use the default method of path replacement. public static (FullPath?, object?) DefaultResolver( Utf8GamePath path ) { - var resolved = Mods.Mod.Manager.ResolvePath( path ); + var resolved = Penumbra.CollectionManager.Default.ResolvePath( path ); return ( resolved, null ); } diff --git a/Penumbra/Meta/Manager/MetaManager.Cmp.cs b/Penumbra/Meta/Manager/MetaManager.Cmp.cs index ce838fe9..e591a0b6 100644 --- a/Penumbra/Meta/Manager/MetaManager.Cmp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Cmp.cs @@ -41,8 +41,8 @@ public partial class MetaManager public bool ApplyMod( RspManipulation m, int modIdx ) { #if USE_CMP - Manipulations[ m ] = modIdx; - File ??= new CmpFile(); + Manipulations[ m ] = modIdx; + File ??= new CmpFile(); return m.Apply( File ); #else return false; diff --git a/Penumbra/Meta/Manager/MetaManager.Eqp.cs b/Penumbra/Meta/Manager/MetaManager.Eqp.cs index 92d8f3d8..053e8228 100644 --- a/Penumbra/Meta/Manager/MetaManager.Eqp.cs +++ b/Penumbra/Meta/Manager/MetaManager.Eqp.cs @@ -41,8 +41,8 @@ public partial class MetaManager public bool ApplyMod( EqpManipulation m, int modIdx ) { #if USE_EQP - Manipulations[ m ] = modIdx; - File ??= new ExpandedEqpFile(); + Manipulations[ m ] = modIdx; + File ??= new ExpandedEqpFile(); return m.Apply( File ); #else return false; diff --git a/Penumbra/Meta/Manager/MetaManager.Imc.cs b/Penumbra/Meta/Manager/MetaManager.Imc.cs index c921049f..23c83487 100644 --- a/Penumbra/Meta/Manager/MetaManager.Imc.cs +++ b/Penumbra/Meta/Manager/MetaManager.Imc.cs @@ -20,7 +20,7 @@ public partial class MetaManager public readonly Dictionary< ImcManipulation, int > Manipulations = new(); private readonly ModCollection _collection; - private static int _imcManagerCount; + private static int _imcManagerCount; public MetaManagerImc( ModCollection collection ) @@ -102,6 +102,7 @@ public partial class MetaManager Files.Clear(); Manipulations.Clear(); + RestoreDelegate(); } [Conditional( "USE_IMC" )] @@ -141,11 +142,11 @@ public partial class MetaManager if( Penumbra.CollectionManager.ByName( split.ToString(), out var collection ) && collection.HasCache && collection.MetaCache!.Imc.Files.TryGetValue( - Utf8GamePath.FromSpan( path.Span, out var p, false ) ? p : Utf8GamePath.Empty, out var file ) ) + Utf8GamePath.FromSpan( path.Span, out var p ) ? p : Utf8GamePath.Empty, out var file ) ) { PluginLog.Debug( "Loaded {GamePath:l} from file and replaced with IMC from collection {Collection:l}.", path, collection.Name ); - file.Replace( fileDescriptor->ResourceHandle, true); + file.Replace( fileDescriptor->ResourceHandle, true ); file.ChangesSinceLoad = false; } diff --git a/Penumbra/Meta/MetaCollection.cs b/Penumbra/Meta/MetaCollection.cs deleted file mode 100644 index d489702e..00000000 --- a/Penumbra/Meta/MetaCollection.cs +++ /dev/null @@ -1,239 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Dalamud.Logging; -using Newtonsoft.Json; -using Penumbra.GameData.ByteString; -using Penumbra.Importer; -using Penumbra.Meta.Manipulations; -using Penumbra.Mods; - -namespace Penumbra.Meta; - -// Corresponds meta manipulations of any kind with the settings for a mod. -// DefaultData contains all manipulations that are active regardless of option groups. -// GroupData contains a mapping of Group -> { Options -> {Manipulations} }. -public class MetaCollection -{ - public List< MetaManipulation > DefaultData = new(); - public Dictionary< string, Dictionary< string, List< MetaManipulation > > > GroupData = new(); - - - // Store total number of manipulations for some ease of access. - [JsonIgnore] - internal int Count; - - - // Return an enumeration of all active meta manipulations for a given mod with given settings. - public IEnumerable< MetaManipulation > GetManipulationsForConfig( ModSettings settings, ModMeta modMeta ) - { - if( Count == DefaultData.Count ) - { - return DefaultData; - } - - IEnumerable< MetaManipulation > ret = DefaultData; - - foreach( var group in modMeta.Groups ) - { - if( !GroupData.TryGetValue( group.Key, out var metas ) || !settings.Settings.TryGetValue( group.Key, out var setting ) ) - { - continue; - } - - if( group.Value.SelectionType == SelectType.Single ) - { - var settingName = group.Value.Options[ setting ].OptionName; - if( metas.TryGetValue( settingName, out var meta ) ) - { - ret = ret.Concat( meta ); - } - } - else - { - for( var i = 0; i < group.Value.Options.Count; ++i ) - { - var flag = 1 << i; - if( ( setting & flag ) == 0 ) - { - continue; - } - - var settingName = group.Value.Options[ i ].OptionName; - if( metas.TryGetValue( settingName, out var meta ) ) - { - ret = ret.Concat( meta ); - } - } - } - } - - return ret; - } - - // Check that the collection is still basically valid, - // i.e. keep it sorted, and verify that the options stored by name are all still part of the mod, - // and that the contained manipulations are still valid and non-default manipulations. - public bool Validate( ModMeta modMeta ) - { - SortLists(); - foreach( var group in GroupData ) - { - if( !modMeta.Groups.TryGetValue( group.Key, out var options ) ) - { - return false; - } - - foreach( var option in group.Value ) - { - if( options.Options.All( o => o.OptionName != option.Key ) ) - { - return false; - } - - //if( option.Value.Any( manip => defaultFiles.CheckAgainstDefault( manip ) ) ) - //{ - // return false; - //} - } - } // TODO - - return true; //DefaultData.All( manip => !defaultFiles.CheckAgainstDefault( manip ) ); - } - - // Re-sort all manipulations. - private void SortLists() - { - DefaultData.Sort(); - foreach( var list in GroupData.Values.SelectMany( g => g.Values ) ) - { - list.Sort(); - } - } - - // Add a parsed TexTools .meta file to a given option group and option. If group is the empty string, add it to default. - // Creates the option group and the option if necessary. - private void AddMeta( string group, string option, TexToolsMeta meta ) - { - var manipulations = meta.EqpManipulations.Select( m => new MetaManipulation( m ) ) - .Concat( meta.EqdpManipulations.Select( m => new MetaManipulation( m ) ) ) - .Concat( meta.EstManipulations.Select( m => new MetaManipulation( m ) ) ) - .Concat( meta.GmpManipulations.Select( m => new MetaManipulation( m ) ) ) - .Concat( meta.RspManipulations.Select( m => new MetaManipulation( m ) ) ) - .Concat( meta.ImcManipulations.Select( m => new MetaManipulation( m ) ) ).ToList(); - - if( group.Length == 0 ) - { - DefaultData.AddRange( manipulations ); - } - else if( option.Length == 0 ) - { } - else if( !GroupData.TryGetValue( group, out var options ) ) - { - GroupData.Add( group, new Dictionary< string, List< MetaManipulation > >() { { option, manipulations } } ); - } - else if( !options.TryGetValue( option, out var list ) ) - { - options.Add( option, manipulations ); - } - else - { - list.AddRange( manipulations ); - } - - Count += manipulations.Count; - } - - // Update the whole meta collection by reading all TexTools .meta files in a mod directory anew, - // combining them with the given ModMeta. - public void Update( IEnumerable< FullPath > files, DirectoryInfo basePath, ModMeta modMeta ) - { - DefaultData.Clear(); - GroupData.Clear(); - Count = 0; - foreach( var file in files ) - { - var metaData = file.Extension.ToLowerInvariant() switch - { - ".meta" => new TexToolsMeta( File.ReadAllBytes( file.FullName ) ), - ".rgsp" => TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ) ), - _ => TexToolsMeta.Invalid, - }; - - if( metaData.FilePath == string.Empty ) - { - continue; - } - - Utf8RelPath.FromFile( file, basePath, out var path ); - var foundAny = false; - foreach( var (name, group) in modMeta.Groups ) - { - foreach( var option in group.Options.Where( o => o.OptionFiles.ContainsKey( path ) ) ) - { - foundAny = true; - AddMeta( name, option.OptionName, metaData ); - } - } - - if( !foundAny ) - { - AddMeta( string.Empty, string.Empty, metaData ); - } - } - - SortLists(); - } - - public static FileInfo FileName( DirectoryInfo basePath ) - => new(Path.Combine( basePath.FullName, "metadata_manipulations.json" )); - - public void SaveToFile( FileInfo file ) - { - try - { - var text = JsonConvert.SerializeObject( this, Formatting.Indented ); - File.WriteAllText( file.FullName, text ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not write metadata manipulations file to {file.FullName}:\n{e}" ); - } - } - - public static MetaCollection? LoadFromFile( FileInfo file ) - { - if( !file.Exists ) - { - return null; - } - - try - { - var text = File.ReadAllText( file.FullName ); - - var collection = JsonConvert.DeserializeObject< MetaCollection >( text, - new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore } ); - - if( collection != null ) - { - if( collection.DefaultData.Concat( collection.GroupData.Values.SelectMany( kvp => kvp.Values.SelectMany( l => l ) ) ) - .Any( m => m.ManipulationType == MetaManipulation.Type.Unknown || !Enum.IsDefined( m.ManipulationType ) ) ) - { - throw new Exception( "Invalid collection" ); - } - - collection.Count = collection.DefaultData.Count - + collection.GroupData.Values.SelectMany( kvp => kvp.Values ).Sum( l => l.Count ); - } - - return collection; - } - catch( Exception e ) - { - PluginLog.Error( $"Could not load mod metadata manipulations from {file.FullName}:\n{e}" ); - return null; - } - } -} \ No newline at end of file diff --git a/Penumbra/MigrateConfiguration.cs b/Penumbra/MigrateConfiguration.cs index cc2bce8a..b4c05850 100644 --- a/Penumbra/MigrateConfiguration.cs +++ b/Penumbra/MigrateConfiguration.cs @@ -5,7 +5,6 @@ using System.Linq; using Dalamud.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using OtterGui.Filesystem; using Penumbra.Collections; using Penumbra.Mods; @@ -21,7 +20,7 @@ public class MigrateConfiguration public string ForcedCollection = string.Empty; public Dictionary< string, string > CharacterCollections = new(); public Dictionary< string, string > ModSortOrder = new(); - public bool InvertModListOrder = false; + public bool InvertModListOrder; public static void Migrate( Configuration config ) @@ -143,32 +142,28 @@ public class MigrateConfiguration var data = JArray.Parse( text ); var maxPriority = 0; - var dict = new Dictionary< string, ModSettings >(); + var dict = new Dictionary< string, ModSettings2.SavedSettings >(); foreach( var setting in data.Cast< JObject >() ) { var modName = ( string )setting[ "FolderName" ]!; var enabled = ( bool )setting[ "Enabled" ]!; var priority = ( int )setting[ "Priority" ]!; - var settings = setting[ "Settings" ]!.ToObject< Dictionary< string, int > >() - ?? setting[ "Conf" ]!.ToObject< Dictionary< string, int > >(); + var settings = setting[ "Settings" ]!.ToObject< Dictionary< string, uint > >() + ?? setting[ "Conf" ]!.ToObject< Dictionary< string, uint > >(); - dict[ modName ] = new ModSettings() + dict[ modName ] = new ModSettings2.SavedSettings() { Enabled = enabled, Priority = priority, Settings = settings!, }; - ; maxPriority = Math.Max( maxPriority, priority ); } InvertModListOrder = _data[ nameof( InvertModListOrder ) ]?.ToObject< bool >() ?? InvertModListOrder; if( !InvertModListOrder ) { - foreach( var setting in dict.Values ) - { - setting.Priority = maxPriority - setting.Priority; - } + dict = dict.ToDictionary( kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority } ); } defaultCollection = ModCollection.MigrateFromV0( ModCollection.DefaultCollection, dict ); diff --git a/Penumbra/Mods/FullMod.cs b/Penumbra/Mods/FullMod.cs deleted file mode 100644 index 50ee92b5..00000000 --- a/Penumbra/Mods/FullMod.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using Penumbra.GameData.ByteString; - -namespace Penumbra.Mods; - -// A complete Mod containing settings (i.e. dependent on a collection) -// and the resulting cache. -public class FullMod -{ - public ModSettings Settings { get; } - public Mod Data { get; } - - public FullMod( ModSettings settings, Mod data ) - { - Settings = settings; - Data = data; - } - - public bool FixSettings() - => Settings.FixInvalidSettings( Data.Meta ); - - public HashSet< Utf8GamePath > GetFiles( FileInfo file ) - { - var relPath = Utf8RelPath.FromFile( file, Data.BasePath, out var p ) ? p : Utf8RelPath.Empty; - return ModFunctions.GetFilesForConfig( relPath, Settings, Data.Meta ); - } - - public override string ToString() - => Data.Meta.Name; -} \ No newline at end of file diff --git a/Penumbra/Mods/GroupInformation.cs b/Penumbra/Mods/GroupInformation.cs deleted file mode 100644 index f71e0d3c..00000000 --- a/Penumbra/Mods/GroupInformation.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel; -using Newtonsoft.Json; -using Penumbra.GameData.ByteString; -using Penumbra.Util; - -namespace Penumbra.Mods; - -public enum SelectType -{ - Single, - Multi, -} - -public struct Option -{ - public string OptionName; - public string OptionDesc; - - [JsonProperty( ItemConverterType = typeof( SingleOrArrayConverter< Utf8GamePath > ) )] - public Dictionary< Utf8RelPath, HashSet< Utf8GamePath > > OptionFiles; - - public bool AddFile( Utf8RelPath filePath, Utf8GamePath gamePath ) - { - if( OptionFiles.TryGetValue( filePath, out var set ) ) - { - return set.Add( gamePath ); - } - - OptionFiles[ filePath ] = new HashSet< Utf8GamePath > { gamePath }; - return true; - } -} - -public struct OptionGroup -{ - public string GroupName; - - [JsonConverter( typeof( Newtonsoft.Json.Converters.StringEnumConverter ) )] - public SelectType SelectionType; - - public List< Option > Options; - - private bool ApplySingleGroupFiles( Utf8RelPath relPath, int selection, HashSet< Utf8GamePath > 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 ) - { - continue; - } - - if( Options[ i ].OptionFiles.ContainsKey( relPath ) ) - { - return true; - } - } - - return false; - } - - private bool ApplyMultiGroupFiles( Utf8RelPath relPath, int selection, HashSet< Utf8GamePath > paths ) - { - var doNotAdd = false; - for( var i = 0; i < Options.Count; ++i ) - { - if( ( selection & ( 1 << i ) ) != 0 ) - { - if( Options[ i ].OptionFiles.TryGetValue( relPath, out var groupPaths ) ) - { - paths.UnionWith( groupPaths ); - doNotAdd = true; - } - } - else if( Options[ i ].OptionFiles.ContainsKey( relPath ) ) - { - doNotAdd = true; - } - } - - return doNotAdd; - } - - // Adds all game paths from the given option that correspond to the given RelPath to paths, if any exist. - internal bool ApplyGroupFiles( Utf8RelPath relPath, int selection, HashSet< Utf8GamePath > paths ) - { - return SelectionType switch - { - SelectType.Single => ApplySingleGroupFiles( relPath, selection, paths ), - SelectType.Multi => ApplyMultiGroupFiles( relPath, selection, paths ), - _ => throw new InvalidEnumArgumentException( "Invalid option group type." ), - }; - } -} \ No newline at end of file diff --git a/Penumbra/Mods/Manager/Mod2.Manager.cs b/Penumbra/Mods/Manager/Mod2.Manager.cs index 2ba5fe27..c2250d3a 100644 --- a/Penumbra/Mods/Manager/Mod2.Manager.cs +++ b/Penumbra/Mods/Manager/Mod2.Manager.cs @@ -25,7 +25,6 @@ public sealed partial class Mod2 IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public Manager( string modDirectory ) { SetBaseDirectory( modDirectory, true ); diff --git a/Penumbra/Mods/Mod.SortOrder.cs b/Penumbra/Mods/Mod.SortOrder.cs deleted file mode 100644 index 5cf55c5e..00000000 --- a/Penumbra/Mods/Mod.SortOrder.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; - -namespace Penumbra.Mods; - -public partial class Mod -{ - public struct SortOrder : IComparable< SortOrder > - { - public ModFolder ParentFolder { get; set; } - - private string _sortOrderName; - - public string SortOrderName - { - get => _sortOrderName; - set => _sortOrderName = value.Replace( '/', '\\' ); - } - - public string SortOrderPath - => ParentFolder.FullName; - - public string FullName - { - get - { - var path = SortOrderPath; - return path.Length > 0 ? $"{path}/{SortOrderName}" : SortOrderName; - } - } - - public SortOrder( ModFolder parentFolder, string name ) - { - ParentFolder = parentFolder; - _sortOrderName = name.Replace( '/', '\\' ); - } - - public string FullPath - => SortOrderPath.Length > 0 ? $"{SortOrderPath}/{SortOrderName}" : SortOrderName; - - public int CompareTo( SortOrder other ) - => string.Compare( FullPath, other.FullPath, StringComparison.InvariantCultureIgnoreCase ); - } -} \ No newline at end of file diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs deleted file mode 100644 index 9ca93f85..00000000 --- a/Penumbra/Mods/Mod.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Dalamud.Logging; -using Penumbra.GameData.ByteString; - -namespace Penumbra.Mods; - -// Mod contains all permanent information about a mod, -// and is independent of collections or settings. -// It only changes when the user actively changes the mod or their filesystem. -public sealed partial class Mod -{ - public DirectoryInfo BasePath; - public ModMeta Meta; - public ModResources Resources; - - public SortOrder Order; - - public SortedList< string, object? > ChangedItems { get; } = new(); - public string LowerChangedItemsString { get; private set; } = string.Empty; - public FileInfo MetaFile { get; set; } - public int Index { get; private set; } = -1; - - private Mod( ModFolder parentFolder, DirectoryInfo basePath, ModMeta meta, ModResources resources) - { - BasePath = basePath; - Meta = meta; - Resources = resources; - MetaFile = MetaFileInfo( basePath ); - Order = new SortOrder( parentFolder, Meta.Name ); - Order.ParentFolder.AddMod( this ); - ComputeChangedItems(); - } - - public void ComputeChangedItems() - { - var identifier = GameData.GameData.GetIdentifier(); - ChangedItems.Clear(); - foreach( var file in Resources.ModFiles.Select( f => f.ToRelPath( BasePath, out var p ) ? p : Utf8RelPath.Empty ) ) - { - foreach( var path in ModFunctions.GetAllFiles( file, Meta ) ) - { - identifier.Identify( ChangedItems, path.ToGamePath() ); - } - } - - foreach( var path in Meta.FileSwaps.Keys ) - { - identifier.Identify( ChangedItems, path.ToGamePath() ); - } - - LowerChangedItemsString = string.Join( "\0", ChangedItems.Keys.Select( k => k.ToLowerInvariant() ) ); - } - - public static FileInfo MetaFileInfo( DirectoryInfo basePath ) - => new(Path.Combine( basePath.FullName, "meta.json" )); - - public static Mod? LoadMod( ModFolder parentFolder, DirectoryInfo basePath ) - { - basePath.Refresh(); - if( !basePath.Exists ) - { - PluginLog.Error( $"Supplied mod directory {basePath} does not exist." ); - return null; - } - - var metaFile = MetaFileInfo( basePath ); - if( !metaFile.Exists ) - { - PluginLog.Debug( "No mod meta found for {ModLocation}.", basePath.Name ); - return null; - } - - var meta = ModMeta.LoadFromFile( metaFile ); - if( meta == null ) - { - return null; - } - - var data = new ModResources(); - if( data.RefreshModFiles( basePath ).HasFlag( ResourceChange.Meta ) ) - { - data.SetManipulations( meta, basePath ); - } - - return new Mod( parentFolder, basePath, meta, data ); - } - - public void SaveMeta() - => Meta.SaveToFile( MetaFile ); - - public override string ToString() - => Order.FullPath; -} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.BasePath.cs b/Penumbra/Mods/Mod2.BasePath.cs index 5b086b31..765b6106 100644 --- a/Penumbra/Mods/Mod2.BasePath.cs +++ b/Penumbra/Mods/Mod2.BasePath.cs @@ -37,7 +37,7 @@ public partial class Mod2 mod.LoadDefaultOption(); mod.LoadAllGroups(); mod.ComputeChangedItems(); - mod.SetHasOptions(); + mod.SetCounts(); return mod; } diff --git a/Penumbra/Mods/Mod2.Creation.cs b/Penumbra/Mods/Mod2.Creation.cs new file mode 100644 index 00000000..915ace3f --- /dev/null +++ b/Penumbra/Mods/Mod2.Creation.cs @@ -0,0 +1,108 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Penumbra.GameData.ByteString; +using Penumbra.Importer.Models; + +namespace Penumbra.Mods; + +public partial class Mod2 +{ + internal static void CreateMeta( DirectoryInfo directory, string? name, string? author, string? description, string? version, + string? website ) + { + var mod = new Mod2( directory ); + if( name is { Length: 0 } ) + { + mod.Name = name; + } + + if( author != null ) + { + mod.Author = author; + } + + if( description != null ) + { + mod.Description = description; + } + + if( version != null ) + { + mod.Version = version; + } + + if( website != null ) + { + mod.Website = website; + } + + mod.SaveMeta(); + } + + internal static void CreateOptionGroup( DirectoryInfo baseFolder, ModGroup groupData, + int priority, string desc, List< ISubMod > subMods ) + { + switch( groupData.SelectionType ) + { + case SelectType.Multi: + { + var group = new MultiModGroup() + { + Name = groupData.GroupName!, + Description = desc, + Priority = priority, + }; + group.PrioritizedOptions.AddRange( subMods.OfType< SubMod >().Select( ( s, idx ) => ( s, idx ) ) ); + IModGroup.SaveModGroup( group, baseFolder ); + break; + } + case SelectType.Single: + { + var group = new SingleModGroup() + { + Name = groupData.GroupName!, + Description = desc, + Priority = priority, + }; + group.OptionData.AddRange( subMods.OfType< SubMod >() ); + IModGroup.SaveModGroup( group, baseFolder ); + break; + } + } + } + + internal static ISubMod CreateSubMod( DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option ) + { + var list = optionFolder.EnumerateFiles( "*.*", SearchOption.AllDirectories ) + .Select( f => ( Utf8GamePath.FromFile( f, optionFolder, out var gamePath, true ), gamePath, new FullPath( f ) ) ) + .Where( t => t.Item1 ); + + var mod = new SubMod() + { + Name = option.Name!, + }; + foreach( var (_, gamePath, file) in list ) + { + mod.FileData.TryAdd( gamePath, file ); + } + + mod.IncorporateMetaChanges( baseFolder, true ); + return mod; + } + + internal static void CreateDefaultFiles( DirectoryInfo directory ) + { + var mod = new Mod2( directory ); + foreach( var file in mod.FindUnusedFiles() ) + { + if( Utf8GamePath.FromFile( new FileInfo( file.FullName ), directory, out var gamePath, true ) ) + { + mod._default.FileData.TryAdd( gamePath, file ); + } + } + + mod._default.IncorporateMetaChanges( directory, true ); + mod.SaveDefaultMod(); + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Files.cs b/Penumbra/Mods/Mod2.Files.cs index 7ba7c63f..a9353bc1 100644 --- a/Penumbra/Mods/Mod2.Files.cs +++ b/Penumbra/Mods/Mod2.Files.cs @@ -17,19 +17,31 @@ public partial class Mod2 public IReadOnlyList< IModGroup > Groups => _groups; + private readonly SubMod _default = new(); + private readonly List< IModGroup > _groups = new(); + + public int TotalFileCount { get; private set; } + public int TotalSwapCount { get; private set; } + public int TotalManipulations { get; private set; } public bool HasOptions { get; private set; } - private void SetHasOptions() + private void SetCounts() { + TotalFileCount = 0; + TotalSwapCount = 0; + TotalManipulations = 0; + foreach( var s in AllSubMods ) + { + TotalFileCount += s.Files.Count; + TotalSwapCount += s.FileSwaps.Count; + TotalManipulations += s.Manipulations.Count; + } + HasOptions = _groups.Any( o => o is MultiModGroup m && m.PrioritizedOptions.Count > 0 || o is SingleModGroup s && s.OptionData.Count > 1 ); } - - private readonly SubMod _default = new(); - private readonly List< IModGroup > _groups = new(); - public IEnumerable< ISubMod > AllSubMods => _groups.SelectMany( o => o ).Prepend( _default ); @@ -56,6 +68,13 @@ public partial class Mod2 .ToList(); } + // Filter invalid files. + // If audio streaming is not disabled, replacing .scd files crashes the game, + // so only add those files if it is disabled. + public static bool FilterFile( Utf8GamePath gamePath ) + => !Penumbra.Config.DisableSoundStreaming + && gamePath.Path.EndsWith( '.', 's', 'c', 'd' ); + public List< FullPath > FindMissingFiles() => AllFiles.Where( f => !f.Exists ).ToList(); diff --git a/Penumbra/Mods/Mod2.Meta.Migration.cs b/Penumbra/Mods/Mod2.Meta.Migration.cs index f7a5e63f..9623487e 100644 --- a/Penumbra/Mods/Mod2.Meta.Migration.cs +++ b/Penumbra/Mods/Mod2.Meta.Migration.cs @@ -49,7 +49,7 @@ public sealed partial class Mod2 mod._default.FileSwapData.Add( gamePath, swapPath ); } - HandleMetaChanges( mod._default, mod.BasePath ); + mod._default.IncorporateMetaChanges( mod.BasePath, false ); foreach( var group in mod.Groups ) { IModGroup.SaveModGroup( group, mod.BasePath ); @@ -119,78 +119,11 @@ public sealed partial class Mod2 } } - private static void HandleMetaChanges( SubMod subMod, DirectoryInfo basePath ) - { - foreach( var (key, file) in subMod.Files.ToList() ) - { - try - { - switch( file.Extension ) - { - case ".meta": - subMod.FileData.Remove( key ); - if( !file.Exists ) - { - continue; - } - - var meta = new TexToolsMeta( File.ReadAllBytes( file.FullName ) ); - foreach( var manip in meta.EqpManipulations ) - { - subMod.ManipulationData.Add( manip ); - } - - foreach( var manip in meta.EqdpManipulations ) - { - subMod.ManipulationData.Add( manip ); - } - - foreach( var manip in meta.EstManipulations ) - { - subMod.ManipulationData.Add( manip ); - } - - foreach( var manip in meta.GmpManipulations ) - { - subMod.ManipulationData.Add( manip ); - } - - foreach( var manip in meta.ImcManipulations ) - { - subMod.ManipulationData.Add( manip ); - } - - break; - case ".rgsp": - subMod.FileData.Remove( key ); - if( !file.Exists ) - { - continue; - } - - var rgsp = TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ) ); - foreach( var manip in rgsp.RspManipulations ) - { - subMod.ManipulationData.Add( manip ); - } - - break; - default: continue; - } - } - catch( Exception e ) - { - PluginLog.Error( $"Could not migrate meta changes in mod {basePath} from file {file.FullName}:\n{e}" ); - continue; - } - } - } - private static SubMod SubModFromOption( DirectoryInfo basePath, OptionV0 option ) { var subMod = new SubMod() { Name = option.OptionName }; AddFilesToSubMod( subMod, basePath, option ); - HandleMetaChanges( subMod, basePath ); + subMod.IncorporateMetaChanges( basePath, false ); return subMod; } diff --git a/Penumbra/Mods/ModCleanup.cs b/Penumbra/Mods/ModCleanup.cs index d09a26b8..80e7632b 100644 --- a/Penumbra/Mods/ModCleanup.cs +++ b/Penumbra/Mods/ModCleanup.cs @@ -12,520 +12,520 @@ using Penumbra.Util; namespace Penumbra.Mods; -public class ModCleanup -{ - private const string Duplicates = "Duplicates"; - private const string Required = "Required"; - - private readonly DirectoryInfo _baseDir; - private readonly ModMeta _mod; - private SHA256? _hasher; - - private readonly Dictionary< long, List< FileInfo > > _filesBySize = new(); - - private SHA256 Sha() - { - _hasher ??= SHA256.Create(); - return _hasher; - } - - private ModCleanup( DirectoryInfo baseDir, ModMeta mod ) - { - _baseDir = baseDir; - _mod = mod; - BuildDict(); - } - - private void BuildDict() - { - foreach( var file in _baseDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) - { - var fileLength = file.Length; - if( _filesBySize.TryGetValue( fileLength, out var files ) ) - { - files.Add( file ); - } - else - { - _filesBySize[ fileLength ] = new List< FileInfo > { file }; - } - } - } - - private static DirectoryInfo CreateNewModDir( Mod mod, string optionGroup, string option ) - { - var newName = $"{mod.BasePath.Name}_{optionGroup}_{option}"; - return TexToolsImport.CreateModFolder( new DirectoryInfo( Penumbra.Config.ModDirectory ), newName ); - } - - private static Mod CreateNewMod( DirectoryInfo newDir, string newSortOrder ) - { - var idx = Penumbra.ModManager.AddMod( newDir ); - var newMod = Penumbra.ModManager.Mods[ idx ]; - newMod.Move( newSortOrder ); - newMod.ComputeChangedItems(); - ModFileSystem.InvokeChange(); - return newMod; - } - - private static ModMeta CreateNewMeta( DirectoryInfo newDir, Mod mod, string name, string optionGroup, string option ) - { - var newMeta = new ModMeta - { - Author = mod.Meta.Author, - Name = name, - Description = $"Split from {mod.Meta.Name} Group {optionGroup} Option {option}.", - }; - var metaFile = new FileInfo( Path.Combine( newDir.FullName, "meta.json" ) ); - newMeta.SaveToFile( metaFile ); - return newMeta; - } - - private static void CreateModSplit( HashSet< string > unseenPaths, Mod mod, OptionGroup group, Option option ) - { - try - { - var newDir = CreateNewModDir( mod, group.GroupName, option.OptionName ); - var newName = group.SelectionType == SelectType.Multi ? $"{group.GroupName} - {option.OptionName}" : option.OptionName; - var newMeta = CreateNewMeta( newDir, mod, newName, group.GroupName, option.OptionName ); - foreach( var (fileName, paths) in option.OptionFiles ) - { - var oldPath = Path.Combine( mod.BasePath.FullName, fileName.ToString() ); - unseenPaths.Remove( oldPath ); - if( File.Exists( oldPath ) ) - { - foreach( var path in paths ) - { - var newPath = Path.Combine( newDir.FullName, path.ToString() ); - Directory.CreateDirectory( Path.GetDirectoryName( newPath )! ); - File.Copy( oldPath, newPath, true ); - } - } - } - - var newSortOrder = group.SelectionType == SelectType.Single - ? $"{mod.Order.ParentFolder.FullName}/{mod.Meta.Name}/{group.GroupName}/{option.OptionName}" - : $"{mod.Order.ParentFolder.FullName}/{mod.Meta.Name}/{group.GroupName} - {option.OptionName}"; - CreateNewMod( newDir, newSortOrder ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not split Mod:\n{e}" ); - } - } - - public static void SplitMod( Mod mod ) - { - if( mod.Meta.Groups.Count == 0 ) - { - return; - } - - var unseenPaths = mod.Resources.ModFiles.Select( f => f.FullName ).ToHashSet(); - foreach( var group in mod.Meta.Groups.Values ) - { - foreach( var option in group.Options ) - { - CreateModSplit( unseenPaths, mod, group, option ); - } - } - - if( unseenPaths.Count == 0 ) - { - return; - } - - var defaultGroup = new OptionGroup() - { - GroupName = "Default", - SelectionType = SelectType.Multi, - }; - var defaultOption = new Option() - { - OptionName = "Files", - OptionFiles = unseenPaths.ToDictionary( - p => Utf8RelPath.FromFile( new FileInfo( p ), mod.BasePath, out var rel ) ? rel : Utf8RelPath.Empty, - p => new HashSet< Utf8GamePath >() - { Utf8GamePath.FromFile( new FileInfo( p ), mod.BasePath, out var game, true ) ? game : Utf8GamePath.Empty } ), - }; - CreateModSplit( unseenPaths, mod, defaultGroup, defaultOption ); - } - - private static Option FindOrCreateDuplicates( ModMeta meta ) - { - static Option RequiredOption() - => new() - { - OptionName = Required, - OptionDesc = "", - OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(), - }; - - if( meta.Groups.TryGetValue( Duplicates, out var duplicates ) ) - { - var idx = duplicates.Options.FindIndex( o => o.OptionName == Required ); - if( idx >= 0 ) - { - return duplicates.Options[ idx ]; - } - - duplicates.Options.Add( RequiredOption() ); - return duplicates.Options.Last(); - } - - meta.Groups.Add( Duplicates, new OptionGroup - { - GroupName = Duplicates, - SelectionType = SelectType.Single, - Options = new List< Option > { RequiredOption() }, - } ); - - return meta.Groups[ Duplicates ].Options.First(); - } - - public static void Deduplicate( DirectoryInfo baseDir, ModMeta mod ) - { - var dedup = new ModCleanup( baseDir, mod ); - foreach( var (key, value) in dedup._filesBySize.Where( pair => pair.Value.Count >= 2 ) ) - { - if( value.Count == 2 ) - { - if( CompareFilesDirectly( value[ 0 ], value[ 1 ] ) ) - { - dedup.ReplaceFile( value[ 0 ], value[ 1 ] ); - } - } - else - { - var deleted = Enumerable.Repeat( false, value.Count ).ToArray(); - var hashes = value.Select( dedup.ComputeHash ).ToArray(); - - for( var i = 0; i < value.Count; ++i ) - { - if( deleted[ i ] ) - { - continue; - } - - for( var j = i + 1; j < value.Count; ++j ) - { - if( deleted[ j ] || !CompareHashes( hashes[ i ], hashes[ j ] ) ) - { - continue; - } - - dedup.ReplaceFile( value[ i ], value[ j ] ); - deleted[ j ] = true; - } - } - } - } - - CleanUpDuplicates( mod ); - ClearEmptySubDirectories( dedup._baseDir ); - } - - private void ReplaceFile( FileInfo f1, FileInfo f2 ) - { - if( !Utf8RelPath.FromFile( f1, _baseDir, out var relName1 ) - || !Utf8RelPath.FromFile( f2, _baseDir, out var relName2 ) ) - { - return; - } - - var inOption1 = false; - var inOption2 = false; - foreach( var option in _mod.Groups.SelectMany( g => g.Value.Options ) ) - { - if( option.OptionFiles.ContainsKey( relName1 ) ) - { - inOption1 = true; - } - - if( !option.OptionFiles.TryGetValue( relName2, out var values ) ) - { - continue; - } - - inOption2 = true; - - foreach( var value in values ) - { - option.AddFile( relName1, value ); - } - - option.OptionFiles.Remove( relName2 ); - } - - if( !inOption1 || !inOption2 ) - { - var duplicates = FindOrCreateDuplicates( _mod ); - if( !inOption1 ) - { - duplicates.AddFile( relName1, relName2.ToGamePath() ); - } - - if( !inOption2 ) - { - duplicates.AddFile( relName1, relName1.ToGamePath() ); - } - } - - PluginLog.Information( $"File {relName1} and {relName2} are identical. Deleting the second." ); - f2.Delete(); - } - - public static bool CompareFilesDirectly( FileInfo f1, FileInfo f2 ) - => File.ReadAllBytes( f1.FullName ).SequenceEqual( File.ReadAllBytes( f2.FullName ) ); - - public static bool CompareHashes( byte[] f1, byte[] f2 ) - => StructuralComparisons.StructuralEqualityComparer.Equals( f1, f2 ); - - public byte[] ComputeHash( FileInfo f ) - { - var stream = File.OpenRead( f.FullName ); - var ret = Sha().ComputeHash( stream ); - stream.Dispose(); - return ret; - } - - // Does not delete the base directory itself even if it is completely empty at the end. - public static void ClearEmptySubDirectories( DirectoryInfo baseDir ) - { - foreach( var subDir in baseDir.GetDirectories() ) - { - ClearEmptySubDirectories( subDir ); - if( subDir.GetFiles().Length == 0 && subDir.GetDirectories().Length == 0 ) - { - subDir.Delete(); - } - } - } - - private static bool FileIsInAnyGroup( ModMeta meta, Utf8RelPath relPath, bool exceptDuplicates = false ) - { - var groupEnumerator = exceptDuplicates - ? meta.Groups.Values.Where( g => g.GroupName != Duplicates ) - : meta.Groups.Values; - return groupEnumerator.SelectMany( group => group.Options ) - .Any( option => option.OptionFiles.ContainsKey( relPath ) ); - } - - private static void CleanUpDuplicates( ModMeta meta ) - { - if( !meta.Groups.TryGetValue( Duplicates, out var info ) ) - { - return; - } - - var requiredIdx = info.Options.FindIndex( o => o.OptionName == Required ); - if( requiredIdx >= 0 ) - { - var required = info.Options[ requiredIdx ]; - foreach( var (key, value) in required.OptionFiles.ToArray() ) - { - if( value.Count > 1 || FileIsInAnyGroup( meta, key, true ) ) - { - continue; - } - - if( value.Count == 0 || value.First().CompareTo( key.ToGamePath() ) == 0 ) - { - required.OptionFiles.Remove( key ); - } - } - - if( required.OptionFiles.Count == 0 ) - { - info.Options.RemoveAt( requiredIdx ); - } - } - - if( info.Options.Count == 0 ) - { - meta.Groups.Remove( Duplicates ); - } - } - - public enum GroupType - { - Both = 0, - Single = 1, - Multi = 2, - }; - - private static void RemoveFromGroups( ModMeta meta, Utf8RelPath relPath, Utf8GamePath gamePath, GroupType type = GroupType.Both, - bool skipDuplicates = true ) - { - if( meta.Groups.Count == 0 ) - { - return; - } - - var enumerator = type switch - { - GroupType.Both => meta.Groups.Values, - GroupType.Single => meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single ), - GroupType.Multi => meta.Groups.Values.Where( g => g.SelectionType == SelectType.Multi ), - _ => throw new InvalidEnumArgumentException( "Invalid Enum in RemoveFromGroups" ), - }; - foreach( var group in enumerator ) - { - var optionEnum = skipDuplicates - ? group.Options.Where( o => group.GroupName != Duplicates || o.OptionName != Required ) - : group.Options; - foreach( var option in optionEnum ) - { - if( option.OptionFiles.TryGetValue( relPath, out var gamePaths ) && gamePaths.Remove( gamePath ) && gamePaths.Count == 0 ) - { - option.OptionFiles.Remove( relPath ); - } - } - } - } - - public static bool MoveFile( ModMeta meta, string basePath, Utf8RelPath oldRelPath, Utf8RelPath newRelPath ) - { - if( oldRelPath.Equals( newRelPath ) ) - { - return true; - } - - try - { - var newFullPath = Path.Combine( basePath, newRelPath.ToString() ); - new FileInfo( newFullPath ).Directory!.Create(); - File.Move( Path.Combine( basePath, oldRelPath.ToString() ), newFullPath ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not move file from {oldRelPath} to {newRelPath}:\n{e}" ); - return false; - } - - foreach( var option in meta.Groups.Values.SelectMany( group => group.Options ) ) - { - if( option.OptionFiles.TryGetValue( oldRelPath, out var gamePaths ) ) - { - option.OptionFiles.Add( newRelPath, gamePaths ); - option.OptionFiles.Remove( oldRelPath ); - } - } - - return true; - } - - - private static void RemoveUselessGroups( ModMeta meta ) - { - meta.Groups = meta.Groups.Where( kvp => kvp.Value.Options.Any( o => o.OptionFiles.Count > 0 ) ) - .ToDictionary( kvp => kvp.Key, kvp => kvp.Value ); - } - - // Goes through all Single-Select options and checks if file links are in each of them. - // If they are, it moves those files to the root folder and removes them from the groups (and puts them to duplicates, if necessary). - public static void Normalize( DirectoryInfo baseDir, ModMeta meta ) - { - foreach( var group in meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single && g.GroupName != Duplicates ) ) - { - var firstOption = true; - HashSet< (Utf8RelPath, Utf8GamePath) > groupList = new(); - foreach( var option in group.Options ) - { - HashSet< (Utf8RelPath, Utf8GamePath) > optionList = new(); - foreach( var (file, gamePaths) in option.OptionFiles.Select( p => ( p.Key, p.Value ) ) ) - { - optionList.UnionWith( gamePaths.Select( p => ( file, p ) ) ); - } - - if( firstOption ) - { - groupList = optionList; - } - else - { - groupList.IntersectWith( optionList ); - } - - firstOption = false; - } - - var newPath = new Dictionary< Utf8RelPath, Utf8GamePath >(); - foreach( var (path, gamePath) in groupList ) - { - var relPath = new Utf8RelPath( gamePath ); - if( newPath.TryGetValue( path, out var usedGamePath ) ) - { - var required = FindOrCreateDuplicates( meta ); - var usedRelPath = new Utf8RelPath( usedGamePath ); - required.AddFile( usedRelPath, gamePath ); - required.AddFile( usedRelPath, usedGamePath ); - RemoveFromGroups( meta, relPath, gamePath, GroupType.Single ); - } - else if( MoveFile( meta, baseDir.FullName, path, relPath ) ) - { - newPath[ path ] = gamePath; - if( FileIsInAnyGroup( meta, relPath ) ) - { - FindOrCreateDuplicates( meta ).AddFile( relPath, gamePath ); - } - - RemoveFromGroups( meta, relPath, gamePath, GroupType.Single ); - } - } - } - - RemoveUselessGroups( meta ); - ClearEmptySubDirectories( baseDir ); - } - - public static void AutoGenerateGroups( DirectoryInfo baseDir, ModMeta meta ) - { - meta.Groups.Clear(); - ClearEmptySubDirectories( baseDir ); - foreach( var groupDir in baseDir.EnumerateDirectories() ) - { - var group = new OptionGroup - { - GroupName = groupDir.Name, - SelectionType = SelectType.Single, - Options = new List< Option >(), - }; - - foreach( var optionDir in groupDir.EnumerateDirectories() ) - { - var option = new Option - { - OptionDesc = string.Empty, - OptionName = optionDir.Name, - OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(), - }; - foreach( var file in optionDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) - { - if( Utf8RelPath.FromFile( file, baseDir, out var rel ) - && Utf8GamePath.FromFile( file, optionDir, out var game ) ) - { - option.OptionFiles[ rel ] = new HashSet< Utf8GamePath > { game }; - } - } - - if( option.OptionFiles.Count > 0 ) - { - group.Options.Add( option ); - } - } - - if( group.Options.Count > 0 ) - { - meta.Groups.Add( groupDir.Name, group ); - } - } - - // TODO - var idx = Penumbra.ModManager.Mods.IndexOf( m => m.Meta == meta ); - foreach( var collection in Penumbra.CollectionManager ) - { - collection.Settings[ idx ]?.FixInvalidSettings( meta ); - } - } -} \ No newline at end of file +//ublic class ModCleanup +// +// private const string Duplicates = "Duplicates"; +// private const string Required = "Required"; +// +// private readonly DirectoryInfo _baseDir; +// private readonly ModMeta _mod; +// private SHA256? _hasher; +// +// private readonly Dictionary< long, List< FileInfo > > _filesBySize = new(); +// +// private SHA256 Sha() +// { +// _hasher ??= SHA256.Create(); +// return _hasher; +// } +// +// private ModCleanup( DirectoryInfo baseDir, ModMeta mod ) +// { +// _baseDir = baseDir; +// _mod = mod; +// BuildDict(); +// } +// +// private void BuildDict() +// { +// foreach( var file in _baseDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) +// { +// var fileLength = file.Length; +// if( _filesBySize.TryGetValue( fileLength, out var files ) ) +// { +// files.Add( file ); +// } +// else +// { +// _filesBySize[ fileLength ] = new List< FileInfo > { file }; +// } +// } +// } +// +// private static DirectoryInfo CreateNewModDir( Mod mod, string optionGroup, string option ) +// { +// var newName = $"{mod.BasePath.Name}_{optionGroup}_{option}"; +// return TexToolsImport.CreateModFolder( new DirectoryInfo( Penumbra.Config.ModDirectory ), newName ); +// } +// +// private static Mod CreateNewMod( DirectoryInfo newDir, string newSortOrder ) +// { +// var idx = Penumbra.ModManager.AddMod( newDir ); +// var newMod = Penumbra.ModManager.Mods[ idx ]; +// newMod.Move( newSortOrder ); +// newMod.ComputeChangedItems(); +// ModFileSystem.InvokeChange(); +// return newMod; +// } +// +// private static ModMeta CreateNewMeta( DirectoryInfo newDir, Mod mod, string name, string optionGroup, string option ) +// { +// var newMeta = new ModMeta +// { +// Author = mod.Meta.Author, +// Name = name, +// Description = $"Split from {mod.Meta.Name} Group {optionGroup} Option {option}.", +// }; +// var metaFile = new FileInfo( Path.Combine( newDir.FullName, "meta.json" ) ); +// newMeta.SaveToFile( metaFile ); +// return newMeta; +// } +// +// private static void CreateModSplit( HashSet< string > unseenPaths, Mod mod, OptionGroup group, Option option ) +// { +// try +// { +// var newDir = CreateNewModDir( mod, group.GroupName, option.OptionName ); +// var newName = group.SelectionType == SelectType.Multi ? $"{group.GroupName} - {option.OptionName}" : option.OptionName; +// var newMeta = CreateNewMeta( newDir, mod, newName, group.GroupName, option.OptionName ); +// foreach( var (fileName, paths) in option.OptionFiles ) +// { +// var oldPath = Path.Combine( mod.BasePath.FullName, fileName.ToString() ); +// unseenPaths.Remove( oldPath ); +// if( File.Exists( oldPath ) ) +// { +// foreach( var path in paths ) +// { +// var newPath = Path.Combine( newDir.FullName, path.ToString() ); +// Directory.CreateDirectory( Path.GetDirectoryName( newPath )! ); +// File.Copy( oldPath, newPath, true ); +// } +// } +// } +// +// var newSortOrder = group.SelectionType == SelectType.Single +// ? $"{mod.Order.ParentFolder.FullName}/{mod.Meta.Name}/{group.GroupName}/{option.OptionName}" +// : $"{mod.Order.ParentFolder.FullName}/{mod.Meta.Name}/{group.GroupName} - {option.OptionName}"; +// CreateNewMod( newDir, newSortOrder ); +// } +// catch( Exception e ) +// { +// PluginLog.Error( $"Could not split Mod:\n{e}" ); +// } +// } +// +// public static void SplitMod( Mod mod ) +// { +// if( mod.Meta.Groups.Count == 0 ) +// { +// return; +// } +// +// var unseenPaths = mod.Resources.ModFiles.Select( f => f.FullName ).ToHashSet(); +// foreach( var group in mod.Meta.Groups.Values ) +// { +// foreach( var option in group.Options ) +// { +// CreateModSplit( unseenPaths, mod, group, option ); +// } +// } +// +// if( unseenPaths.Count == 0 ) +// { +// return; +// } +// +// var defaultGroup = new OptionGroup() +// { +// GroupName = "Default", +// SelectionType = SelectType.Multi, +// }; +// var defaultOption = new Option() +// { +// OptionName = "Files", +// OptionFiles = unseenPaths.ToDictionary( +// p => Utf8RelPath.FromFile( new FileInfo( p ), mod.BasePath, out var rel ) ? rel : Utf8RelPath.Empty, +// p => new HashSet< Utf8GamePath >() +// { Utf8GamePath.FromFile( new FileInfo( p ), mod.BasePath, out var game, true ) ? game : Utf8GamePath.Empty } ), +// }; +// CreateModSplit( unseenPaths, mod, defaultGroup, defaultOption ); +// } +// +// private static Option FindOrCreateDuplicates( ModMeta meta ) +// { +// static Option RequiredOption() +// => new() +// { +// OptionName = Required, +// OptionDesc = "", +// OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(), +// }; +// +// if( meta.Groups.TryGetValue( Duplicates, out var duplicates ) ) +// { +// var idx = duplicates.Options.FindIndex( o => o.OptionName == Required ); +// if( idx >= 0 ) +// { +// return duplicates.Options[ idx ]; +// } +// +// duplicates.Options.Add( RequiredOption() ); +// return duplicates.Options.Last(); +// } +// +// meta.Groups.Add( Duplicates, new OptionGroup +// { +// GroupName = Duplicates, +// SelectionType = SelectType.Single, +// Options = new List< Option > { RequiredOption() }, +// } ); +// +// return meta.Groups[ Duplicates ].Options.First(); +// } +// +// public static void Deduplicate( DirectoryInfo baseDir, ModMeta mod ) +// { +// var dedup = new ModCleanup( baseDir, mod ); +// foreach( var (key, value) in dedup._filesBySize.Where( pair => pair.Value.Count >= 2 ) ) +// { +// if( value.Count == 2 ) +// { +// if( CompareFilesDirectly( value[ 0 ], value[ 1 ] ) ) +// { +// dedup.ReplaceFile( value[ 0 ], value[ 1 ] ); +// } +// } +// else +// { +// var deleted = Enumerable.Repeat( false, value.Count ).ToArray(); +// var hashes = value.Select( dedup.ComputeHash ).ToArray(); +// +// for( var i = 0; i < value.Count; ++i ) +// { +// if( deleted[ i ] ) +// { +// continue; +// } +// +// for( var j = i + 1; j < value.Count; ++j ) +// { +// if( deleted[ j ] || !CompareHashes( hashes[ i ], hashes[ j ] ) ) +// { +// continue; +// } +// +// dedup.ReplaceFile( value[ i ], value[ j ] ); +// deleted[ j ] = true; +// } +// } +// } +// } +// +// CleanUpDuplicates( mod ); +// ClearEmptySubDirectories( dedup._baseDir ); +// } +// +// private void ReplaceFile( FileInfo f1, FileInfo f2 ) +// { +// if( !Utf8RelPath.FromFile( f1, _baseDir, out var relName1 ) +// || !Utf8RelPath.FromFile( f2, _baseDir, out var relName2 ) ) +// { +// return; +// } +// +// var inOption1 = false; +// var inOption2 = false; +// foreach( var option in _mod.Groups.SelectMany( g => g.Value.Options ) ) +// { +// if( option.OptionFiles.ContainsKey( relName1 ) ) +// { +// inOption1 = true; +// } +// +// if( !option.OptionFiles.TryGetValue( relName2, out var values ) ) +// { +// continue; +// } +// +// inOption2 = true; +// +// foreach( var value in values ) +// { +// option.AddFile( relName1, value ); +// } +// +// option.OptionFiles.Remove( relName2 ); +// } +// +// if( !inOption1 || !inOption2 ) +// { +// var duplicates = FindOrCreateDuplicates( _mod ); +// if( !inOption1 ) +// { +// duplicates.AddFile( relName1, relName2.ToGamePath() ); +// } +// +// if( !inOption2 ) +// { +// duplicates.AddFile( relName1, relName1.ToGamePath() ); +// } +// } +// +// PluginLog.Information( $"File {relName1} and {relName2} are identical. Deleting the second." ); +// f2.Delete(); +// } +// +// public static bool CompareFilesDirectly( FileInfo f1, FileInfo f2 ) +// => File.ReadAllBytes( f1.FullName ).SequenceEqual( File.ReadAllBytes( f2.FullName ) ); +// +// public static bool CompareHashes( byte[] f1, byte[] f2 ) +// => StructuralComparisons.StructuralEqualityComparer.Equals( f1, f2 ); +// +// public byte[] ComputeHash( FileInfo f ) +// { +// var stream = File.OpenRead( f.FullName ); +// var ret = Sha().ComputeHash( stream ); +// stream.Dispose(); +// return ret; +// } +// +// // Does not delete the base directory itself even if it is completely empty at the end. +// public static void ClearEmptySubDirectories( DirectoryInfo baseDir ) +// { +// foreach( var subDir in baseDir.GetDirectories() ) +// { +// ClearEmptySubDirectories( subDir ); +// if( subDir.GetFiles().Length == 0 && subDir.GetDirectories().Length == 0 ) +// { +// subDir.Delete(); +// } +// } +// } +// +// private static bool FileIsInAnyGroup( ModMeta meta, Utf8RelPath relPath, bool exceptDuplicates = false ) +// { +// var groupEnumerator = exceptDuplicates +// ? meta.Groups.Values.Where( g => g.GroupName != Duplicates ) +// : meta.Groups.Values; +// return groupEnumerator.SelectMany( group => group.Options ) +// .Any( option => option.OptionFiles.ContainsKey( relPath ) ); +// } +// +// private static void CleanUpDuplicates( ModMeta meta ) +// { +// if( !meta.Groups.TryGetValue( Duplicates, out var info ) ) +// { +// return; +// } +// +// var requiredIdx = info.Options.FindIndex( o => o.OptionName == Required ); +// if( requiredIdx >= 0 ) +// { +// var required = info.Options[ requiredIdx ]; +// foreach( var (key, value) in required.OptionFiles.ToArray() ) +// { +// if( value.Count > 1 || FileIsInAnyGroup( meta, key, true ) ) +// { +// continue; +// } +// +// if( value.Count == 0 || value.First().CompareTo( key.ToGamePath() ) == 0 ) +// { +// required.OptionFiles.Remove( key ); +// } +// } +// +// if( required.OptionFiles.Count == 0 ) +// { +// info.Options.RemoveAt( requiredIdx ); +// } +// } +// +// if( info.Options.Count == 0 ) +// { +// meta.Groups.Remove( Duplicates ); +// } +// } +// +// public enum GroupType +// { +// Both = 0, +// Single = 1, +// Multi = 2, +// }; +// +// private static void RemoveFromGroups( ModMeta meta, Utf8RelPath relPath, Utf8GamePath gamePath, GroupType type = GroupType.Both, +// bool skipDuplicates = true ) +// { +// if( meta.Groups.Count == 0 ) +// { +// return; +// } +// +// var enumerator = type switch +// { +// GroupType.Both => meta.Groups.Values, +// GroupType.Single => meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single ), +// GroupType.Multi => meta.Groups.Values.Where( g => g.SelectionType == SelectType.Multi ), +// _ => throw new InvalidEnumArgumentException( "Invalid Enum in RemoveFromGroups" ), +// }; +// foreach( var group in enumerator ) +// { +// var optionEnum = skipDuplicates +// ? group.Options.Where( o => group.GroupName != Duplicates || o.OptionName != Required ) +// : group.Options; +// foreach( var option in optionEnum ) +// { +// if( option.OptionFiles.TryGetValue( relPath, out var gamePaths ) && gamePaths.Remove( gamePath ) && gamePaths.Count == 0 ) +// { +// option.OptionFiles.Remove( relPath ); +// } +// } +// } +// } +// +// public static bool MoveFile( ModMeta meta, string basePath, Utf8RelPath oldRelPath, Utf8RelPath newRelPath ) +// { +// if( oldRelPath.Equals( newRelPath ) ) +// { +// return true; +// } +// +// try +// { +// var newFullPath = Path.Combine( basePath, newRelPath.ToString() ); +// new FileInfo( newFullPath ).Directory!.Create(); +// File.Move( Path.Combine( basePath, oldRelPath.ToString() ), newFullPath ); +// } +// catch( Exception e ) +// { +// PluginLog.Error( $"Could not move file from {oldRelPath} to {newRelPath}:\n{e}" ); +// return false; +// } +// +// foreach( var option in meta.Groups.Values.SelectMany( group => group.Options ) ) +// { +// if( option.OptionFiles.TryGetValue( oldRelPath, out var gamePaths ) ) +// { +// option.OptionFiles.Add( newRelPath, gamePaths ); +// option.OptionFiles.Remove( oldRelPath ); +// } +// } +// +// return true; +// } +// +// +// private static void RemoveUselessGroups( ModMeta meta ) +// { +// meta.Groups = meta.Groups.Where( kvp => kvp.Value.Options.Any( o => o.OptionFiles.Count > 0 ) ) +// .ToDictionary( kvp => kvp.Key, kvp => kvp.Value ); +// } +// +// // Goes through all Single-Select options and checks if file links are in each of them. +// // If they are, it moves those files to the root folder and removes them from the groups (and puts them to duplicates, if necessary). +// public static void Normalize( DirectoryInfo baseDir, ModMeta meta ) +// { +// foreach( var group in meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single && g.GroupName != Duplicates ) ) +// { +// var firstOption = true; +// HashSet< (Utf8RelPath, Utf8GamePath) > groupList = new(); +// foreach( var option in group.Options ) +// { +// HashSet< (Utf8RelPath, Utf8GamePath) > optionList = new(); +// foreach( var (file, gamePaths) in option.OptionFiles.Select( p => ( p.Key, p.Value ) ) ) +// { +// optionList.UnionWith( gamePaths.Select( p => ( file, p ) ) ); +// } +// +// if( firstOption ) +// { +// groupList = optionList; +// } +// else +// { +// groupList.IntersectWith( optionList ); +// } +// +// firstOption = false; +// } +// +// var newPath = new Dictionary< Utf8RelPath, Utf8GamePath >(); +// foreach( var (path, gamePath) in groupList ) +// { +// var relPath = new Utf8RelPath( gamePath ); +// if( newPath.TryGetValue( path, out var usedGamePath ) ) +// { +// var required = FindOrCreateDuplicates( meta ); +// var usedRelPath = new Utf8RelPath( usedGamePath ); +// required.AddFile( usedRelPath, gamePath ); +// required.AddFile( usedRelPath, usedGamePath ); +// RemoveFromGroups( meta, relPath, gamePath, GroupType.Single ); +// } +// else if( MoveFile( meta, baseDir.FullName, path, relPath ) ) +// { +// newPath[ path ] = gamePath; +// if( FileIsInAnyGroup( meta, relPath ) ) +// { +// FindOrCreateDuplicates( meta ).AddFile( relPath, gamePath ); +// } +// +// RemoveFromGroups( meta, relPath, gamePath, GroupType.Single ); +// } +// } +// } +// +// RemoveUselessGroups( meta ); +// ClearEmptySubDirectories( baseDir ); +// } +// +// public static void AutoGenerateGroups( DirectoryInfo baseDir, ModMeta meta ) +// { +// meta.Groups.Clear(); +// ClearEmptySubDirectories( baseDir ); +// foreach( var groupDir in baseDir.EnumerateDirectories() ) +// { +// var group = new OptionGroup +// { +// GroupName = groupDir.Name, +// SelectionType = SelectType.Single, +// Options = new List< Option >(), +// }; +// +// foreach( var optionDir in groupDir.EnumerateDirectories() ) +// { +// var option = new Option +// { +// OptionDesc = string.Empty, +// OptionName = optionDir.Name, +// OptionFiles = new Dictionary< Utf8RelPath, HashSet< Utf8GamePath > >(), +// }; +// foreach( var file in optionDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) +// { +// if( Utf8RelPath.FromFile( file, baseDir, out var rel ) +// && Utf8GamePath.FromFile( file, optionDir, out var game ) ) +// { +// option.OptionFiles[ rel ] = new HashSet< Utf8GamePath > { game }; +// } +// } +// +// if( option.OptionFiles.Count > 0 ) +// { +// group.Options.Add( option ); +// } +// } +// +// if( group.Options.Count > 0 ) +// { +// meta.Groups.Add( groupDir.Name, group ); +// } +// } +// +// // TODO +// var idx = Penumbra.ModManager.Mods.IndexOf( m => m.Meta == meta ); +// foreach( var collection in Penumbra.CollectionManager ) +// { +// collection.Settings[ idx ]?.FixInvalidSettings( meta ); +// } +// } +// \ No newline at end of file diff --git a/Penumbra/Mods/ModFileSystem.cs b/Penumbra/Mods/ModFileSystem.cs deleted file mode 100644 index c4fbcc89..00000000 --- a/Penumbra/Mods/ModFileSystem.cs +++ /dev/null @@ -1,260 +0,0 @@ -using System; -using System.Linq; - -namespace Penumbra.Mods; - -public delegate void OnModFileSystemChange(); - -public static partial class ModFileSystem -{ - // The root folder that should be used as the base for all structured mods. - public static ModFolder Root = ModFolder.CreateRoot(); - - // Gets invoked every time the file system changes. - public static event OnModFileSystemChange? ModFileSystemChanged; - - internal static void InvokeChange() - => ModFileSystemChanged?.Invoke(); - - // Find a specific mod folder by its path from Root. - // Returns true if the folder was found, and false if not. - // The out parameter will contain the furthest existing folder. - public static bool Find( string path, out ModFolder folder ) - { - var split = path.Split( new[] { '/' }, StringSplitOptions.RemoveEmptyEntries ); - folder = Root; - foreach( var part in split ) - { - if( !folder.FindSubFolder( part, out folder ) ) - { - return false; - } - } - - return true; - } - - // Rename the SortOrderName of a single mod. Slashes are replaced by Backslashes. - // Saves and returns true if anything changed. - public static bool Rename( this global::Penumbra.Mods.Mod mod, string newName ) - { - if( RenameNoSave( mod, newName ) ) - { - SaveMod( mod ); - return true; - } - - return false; - } - - // Rename the target folder, merging it and its subfolders if the new name already exists. - // Saves all mods manipulated thus, and returns true if anything changed. - public static bool Rename( this ModFolder target, string newName ) - { - if( RenameNoSave( target, newName ) ) - { - SaveModChildren( target ); - return true; - } - - return false; - } - - // Move a single mod to the target folder. - // Returns true and saves if anything changed. - public static bool Move( this global::Penumbra.Mods.Mod mod, ModFolder target ) - { - if( MoveNoSave( mod, target ) ) - { - SaveMod( mod ); - return true; - } - - return false; - } - - // Move a mod to the filesystem location specified by sortOrder and rename its SortOrderName. - // Creates all necessary Subfolders. - public static void Move( this global::Penumbra.Mods.Mod mod, string sortOrder ) - { - var split = sortOrder.Split( new[] { '/' }, StringSplitOptions.RemoveEmptyEntries ); - var folder = Root; - for( var i = 0; i < split.Length - 1; ++i ) - { - folder = folder.FindOrCreateSubFolder( split[ i ] ).Item1; - } - - if( MoveNoSave( mod, folder ) | RenameNoSave( mod, split.Last() ) ) - { - SaveMod( mod ); - } - } - - // Moves folder to target. - // If an identically named subfolder of target already exists, merges instead. - // Root is not movable. - public static bool Move( this ModFolder folder, ModFolder target ) - { - if( MoveNoSave( folder, target ) ) - { - SaveModChildren( target ); - return true; - } - - return false; - } - - // Merge source with target, moving all direct mod children of source to target, - // and moving all subfolders of source to target, or merging them with targets subfolders if they exist. - // Returns true and saves if anything changed. - public static bool Merge( this ModFolder source, ModFolder target ) - { - if( MergeNoSave( source, target ) ) - { - SaveModChildren( target ); - return true; - } - - return false; - } -} - -// Internal stuff. -public static partial class ModFileSystem -{ - // Reset all sort orders for all descendants of the given folder. - // Assumes that it is not called on Root, and thus does not remove unnecessary SortOrder entries. - private static void SaveModChildren( ModFolder target ) - { - foreach( var mod in target.AllMods( true ) ) - { - Penumbra.ModManager.TemporaryModSortOrder[ mod.BasePath.Name ] = mod.Order.FullName; - } - - Penumbra.Config.Save(); - InvokeChange(); - } - - // Sets and saves the sort order of a single mod, removing the entry if it is unnecessary. - private static void SaveMod( Mod mod ) - { - if( ReferenceEquals( mod.Order.ParentFolder, Root ) - && string.Equals( mod.Order.SortOrderName, mod.Meta.Name.Text.Replace( '/', '\\' ), StringComparison.InvariantCultureIgnoreCase ) ) - { - Penumbra.ModManager.TemporaryModSortOrder.Remove( mod.BasePath.Name ); - } - else - { - Penumbra.ModManager.TemporaryModSortOrder[ mod.BasePath.Name ] = mod.Order.FullName; - } - - Penumbra.Config.Save(); - InvokeChange(); - } - - private static bool RenameNoSave( this ModFolder target, string newName ) - { - if( ReferenceEquals( target, Root ) ) - { - throw new InvalidOperationException( "Can not rename root." ); - } - - newName = newName.Replace( '/', '\\' ); - if( target.Name == newName ) - { - return false; - } - - ModFolder.FolderComparer.CompareType = StringComparison.InvariantCulture; - if( target.Parent!.FindSubFolder( newName, out var preExisting ) ) - { - MergeNoSave( target, preExisting ); - ModFolder.FolderComparer.CompareType = StringComparison.InvariantCultureIgnoreCase; - } - else - { - ModFolder.FolderComparer.CompareType = StringComparison.InvariantCultureIgnoreCase; - var parent = target.Parent; - parent.RemoveFolderIgnoreEmpty( target ); - target.Name = newName; - parent.FindOrAddSubFolder( target ); - } - - return true; - } - - private static bool RenameNoSave( Mod mod, string newName ) - { - newName = newName.Replace( '/', '\\' ); - if( mod.Order.SortOrderName == newName ) - { - return false; - } - - mod.Order.ParentFolder.RemoveModIgnoreEmpty( mod ); - mod.Order = new Mod.SortOrder( mod.Order.ParentFolder, newName ); - mod.Order.ParentFolder.AddMod( mod ); - return true; - } - - private static bool MoveNoSave( Mod mod, ModFolder target ) - { - var oldParent = mod.Order.ParentFolder; - if( ReferenceEquals( target, oldParent ) ) - { - return false; - } - - oldParent.RemoveMod( mod ); - mod.Order = new Mod.SortOrder( target, mod.Order.SortOrderName ); - target.AddMod( mod ); - return true; - } - - private static bool MergeNoSave( ModFolder source, ModFolder target ) - { - if( ReferenceEquals( source, target ) ) - { - return false; - } - - var any = false; - while( source.SubFolders.Count > 0 ) - { - any |= MoveNoSave( source.SubFolders.First(), target ); - } - - while( source.Mods.Count > 0 ) - { - any |= MoveNoSave( source.Mods.First(), target ); - } - - source.Parent?.RemoveSubFolder( source ); - - return any || source.Parent != null; - } - - private static bool MoveNoSave( ModFolder folder, ModFolder target ) - { - // Moving a folder into itself is not permitted. - if( ReferenceEquals( folder, target ) ) - { - return false; - } - - if( ReferenceEquals( target, folder.Parent! ) ) - { - return false; - } - - folder.Parent!.RemoveSubFolder( folder ); - var subFolderIdx = target.FindOrAddSubFolder( folder ); - if( subFolderIdx > 0 ) - { - var main = target.SubFolders[ subFolderIdx ]; - MergeNoSave( folder, main ); - } - - return true; - } -} \ No newline at end of file diff --git a/Penumbra/Mods/ModFileSystemA.cs b/Penumbra/Mods/ModFileSystemA.cs index c684d371..c2b62921 100644 --- a/Penumbra/Mods/ModFileSystemA.cs +++ b/Penumbra/Mods/ModFileSystemA.cs @@ -4,13 +4,13 @@ using OtterGui.Filesystem; namespace Penumbra.Mods; -public sealed class ModFileSystemA : FileSystem< Mod >, IDisposable +public sealed class ModFileSystemA : FileSystem< Mod2 >, IDisposable { // Save the current sort order. // Does not save or copy the backup in the current mod directory, // as this is done on mod directory changes only. public void Save() - => SaveToFile( new FileInfo( Mod.Manager.SortOrderFile ), SaveMod, true ); + => SaveToFile( new FileInfo( Mod2.Manager.ModFileSystemFile ), SaveMod, true ); // Create a new ModFileSystem from the currently loaded mods and the current sort order file. public static ModFileSystemA Load() @@ -31,7 +31,7 @@ public sealed class ModFileSystemA : FileSystem< Mod >, IDisposable // Used on construction and on mod rediscoveries. private void Reload() { - if( Load( new FileInfo( Mod.Manager.SortOrderFile ), Penumbra.ModManager.Mods, ModToIdentifier, ModToName ) ) + if( Load( new FileInfo( Mod2.Manager.ModFileSystemFile ), Penumbra.ModManager.Mods, ModToIdentifier, ModToName ) ) { Save(); } @@ -47,13 +47,13 @@ public sealed class ModFileSystemA : FileSystem< Mod >, IDisposable } // Used for saving and loading. - private static string ModToIdentifier( Mod mod ) + private static string ModToIdentifier( Mod2 mod ) => mod.BasePath.Name; - private static string ModToName( Mod mod ) - => mod.Meta.Name.Text; + private static string ModToName( Mod2 mod ) + => mod.Name.Text; - private static (string, bool) SaveMod( Mod mod, string fullPath ) + private static (string, bool) SaveMod( Mod2 mod, string fullPath ) { // Only save pairs with non-default paths. if( fullPath == ModToName( mod ) ) diff --git a/Penumbra/Mods/ModFolder.cs b/Penumbra/Mods/ModFolder.cs deleted file mode 100644 index 80a1df51..00000000 --- a/Penumbra/Mods/ModFolder.cs +++ /dev/null @@ -1,245 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Penumbra.Mods; - -public partial class ModFolder -{ - public ModFolder? Parent; - - public string FullName - { - get - { - var parentPath = Parent?.FullName ?? string.Empty; - return parentPath.Any() ? $"{parentPath}/{Name}" : Name; - } - } - - private string _name = string.Empty; - - public string Name - { - get => _name; - set => _name = value.Replace( '/', '\\' ); - } - - public List< ModFolder > SubFolders { get; } = new(); - public List< Mod > Mods { get; } = new(); - - public ModFolder( ModFolder parent, string name ) - { - Parent = parent; - Name = name; - } - - public override string ToString() - => FullName; - - public int TotalDescendantMods() - => Mods.Count + SubFolders.Sum( f => f.TotalDescendantMods() ); - - public int TotalDescendantFolders() - => SubFolders.Sum( f => f.TotalDescendantFolders() ); - - // Return all descendant mods in the specified order. - public IEnumerable< Mod > AllMods( bool foldersFirst ) - { - if( foldersFirst ) - { - return SubFolders.SelectMany( f => f.AllMods( foldersFirst ) ).Concat( Mods ); - } - - return GetSortedEnumerator().SelectMany( f => - { - if( f is ModFolder folder ) - { - return folder.AllMods( false ); - } - - return new[] { ( Mod )f }; - } ); - } - - // Return all descendant subfolders. - public IEnumerable< ModFolder > AllFolders() - => SubFolders.SelectMany( f => f.AllFolders() ).Prepend( this ); - - // Iterate through all descendants in the specified order, returning subfolders as well as mods. - public IEnumerable< object > GetItems( bool foldersFirst ) - => foldersFirst ? SubFolders.Cast< object >().Concat( Mods ) : GetSortedEnumerator(); - - // Find a subfolder by name. Returns true and sets folder to it if it exists. - public bool FindSubFolder( string name, out ModFolder folder ) - { - var subFolder = new ModFolder( this, name ); - var idx = SubFolders.BinarySearch( subFolder, FolderComparer ); - folder = idx >= 0 ? SubFolders[ idx ] : this; - return idx >= 0; - } - - // Checks if an equivalent subfolder as folder already exists and returns its index. - // If it does not exist, inserts folder as a subfolder and returns the new index. - // Also sets this as folders parent. - public int FindOrAddSubFolder( ModFolder folder ) - { - var idx = SubFolders.BinarySearch( folder, FolderComparer ); - if( idx >= 0 ) - { - return idx; - } - - idx = ~idx; - SubFolders.Insert( idx, folder ); - folder.Parent = this; - return idx; - } - - // Checks if a subfolder with the given name already exists and returns it and its index. - // If it does not exists, creates and inserts it and returns the new subfolder and its index. - public (ModFolder, int) FindOrCreateSubFolder( string name ) - { - var subFolder = new ModFolder( this, name ); - var idx = FindOrAddSubFolder( subFolder ); - return ( SubFolders[ idx ], idx ); - } - - // Remove folder as a subfolder if it exists. - // If this folder is empty afterwards, remove it from its parent. - public void RemoveSubFolder( ModFolder folder ) - { - RemoveFolderIgnoreEmpty( folder ); - CheckEmpty(); - } - - // Add the given mod as a child, if it is not already a child. - // Returns the index of the found or inserted mod. - public int AddMod( Mod mod ) - { - var idx = Mods.BinarySearch( mod, ModComparer ); - if( idx >= 0 ) - { - return idx; - } - - idx = ~idx; - Mods.Insert( idx, mod ); - - return idx; - } - - // Remove mod as a child if it exists. - // If this folder is empty afterwards, remove it from its parent. - public void RemoveMod( Mod mod ) - { - RemoveModIgnoreEmpty( mod ); - CheckEmpty(); - } -} - -// Internals -public partial class ModFolder -{ - // Create a Root folder without parent. - internal static ModFolder CreateRoot() - => new(null!, string.Empty); - - internal class ModFolderComparer : IComparer< ModFolder > - { - public StringComparison CompareType = StringComparison.InvariantCultureIgnoreCase; - - // Compare only the direct folder names since this is only used inside an enumeration of subfolders of one folder. - public int Compare( ModFolder? x, ModFolder? y ) - => ReferenceEquals( x, y ) - ? 0 - : string.Compare( x?.Name ?? string.Empty, y?.Name ?? string.Empty, CompareType ); - } - - internal class ModDataComparer : IComparer< Mod > - { - public StringComparison CompareType = StringComparison.InvariantCultureIgnoreCase; - - // Compare only the direct SortOrderNames since this is only used inside an enumeration of direct mod children of one folder. - // Since mod SortOrderNames do not have to be unique inside a folder, also compare their BasePaths (and thus their identity) if necessary. - public int Compare( Mod? x, Mod? y ) - { - if( ReferenceEquals( x, y ) ) - { - return 0; - } - - var cmp = string.Compare( x?.Order.SortOrderName, y?.Order.SortOrderName, CompareType ); - if( cmp != 0 ) - { - return cmp; - } - - return string.Compare( x?.BasePath.Name, y?.BasePath.Name, StringComparison.InvariantCulture ); - } - } - - internal static readonly ModFolderComparer FolderComparer = new(); - internal static readonly ModDataComparer ModComparer = new(); - - // Get an enumerator for actually sorted objects instead of folder-first objects. - private IEnumerable< object > GetSortedEnumerator() - { - var modIdx = 0; - foreach( var folder in SubFolders ) - { - var folderString = folder.Name; - for( ; modIdx < Mods.Count; ++modIdx ) - { - var mod = Mods[ modIdx ]; - var modString = mod.Order.SortOrderName; - if( string.Compare( folderString, modString, StringComparison.InvariantCultureIgnoreCase ) > 0 ) - { - yield return mod; - } - else - { - break; - } - } - - yield return folder; - } - - for( ; modIdx < Mods.Count; ++modIdx ) - { - yield return Mods[ modIdx ]; - } - } - - private void CheckEmpty() - { - if( Mods.Count == 0 && SubFolders.Count == 0 ) - { - Parent?.RemoveSubFolder( this ); - } - } - - // Remove a subfolder but do not remove this folder from its parent if it is empty afterwards. - internal void RemoveFolderIgnoreEmpty( ModFolder folder ) - { - var idx = SubFolders.BinarySearch( folder, FolderComparer ); - if( idx < 0 ) - { - return; - } - - SubFolders[ idx ].Parent = null; - SubFolders.RemoveAt( idx ); - } - - // Remove a mod, but do not remove this folder from its parent if it is empty afterwards. - internal void RemoveModIgnoreEmpty( Mod mod ) - { - var idx = Mods.BinarySearch( mod, ModComparer ); - if( idx >= 0 ) - { - Mods.RemoveAt( idx ); - } - } -} \ No newline at end of file diff --git a/Penumbra/Mods/ModFunctions.cs b/Penumbra/Mods/ModFunctions.cs deleted file mode 100644 index 72787222..00000000 --- a/Penumbra/Mods/ModFunctions.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Penumbra.GameData.ByteString; - -namespace Penumbra.Mods; - -// Functions that do not really depend on only one component of a mod. -public static class ModFunctions -{ - public static bool CleanUpCollection( Dictionary< string, ModSettings > settings, IEnumerable< DirectoryInfo > modPaths ) - { - var hashes = modPaths.Select( p => p.Name ).ToHashSet(); - var missingMods = settings.Keys.Where( k => !hashes.Contains( k ) ).ToArray(); - var anyChanges = false; - foreach( var toRemove in missingMods ) - { - anyChanges |= settings.Remove( toRemove ); - } - - return anyChanges; - } - - public static HashSet< Utf8GamePath > GetFilesForConfig( Utf8RelPath relPath, ModSettings settings, ModMeta meta ) - { - var doNotAdd = false; - var files = new HashSet< Utf8GamePath >(); - foreach( var group in meta.Groups.Values.Where( g => g.Options.Count > 0 ) ) - { - doNotAdd |= group.ApplyGroupFiles( relPath, settings.Settings[ group.GroupName ], files ); - } - - if( !doNotAdd ) - { - files.Add( relPath.ToGamePath() ); - } - - return files; - } - - public static HashSet< Utf8GamePath > GetAllFiles( Utf8RelPath relPath, ModMeta meta ) - { - var ret = new HashSet< Utf8GamePath >(); - foreach( var option in meta.Groups.Values.SelectMany( g => g.Options ) ) - { - if( option.OptionFiles.TryGetValue( relPath, out var files ) ) - { - ret.UnionWith( files ); - } - } - - if( ret.Count == 0 ) - { - ret.Add( relPath.ToGamePath() ); - } - - return ret; - } - - public static ModSettings ConvertNamedSettings( NamedModSettings namedSettings, ModMeta meta ) - { - ModSettings ret = new() - { - Priority = namedSettings.Priority, - Settings = namedSettings.Settings.Keys.ToDictionary( k => k, _ => 0 ), - }; - - foreach( var setting in namedSettings.Settings.Keys ) - { - if( !meta.Groups.TryGetValue( setting, out var info ) ) - { - continue; - } - - if( info.SelectionType == SelectType.Single ) - { - if( namedSettings.Settings[ setting ].Count == 0 ) - { - ret.Settings[ setting ] = 0; - } - else - { - var idx = info.Options.FindIndex( o => o.OptionName == namedSettings.Settings[ setting ].Last() ); - ret.Settings[ setting ] = idx < 0 ? 0 : idx; - } - } - else - { - foreach( var idx in namedSettings.Settings[ setting ] - .Select( option => info.Options.FindIndex( o => o.OptionName == option ) ) - .Where( idx => idx >= 0 ) ) - { - ret.Settings[ setting ] |= 1 << idx; - } - } - } - - return ret; - } -} \ No newline at end of file diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs deleted file mode 100644 index 1f2f5cb7..00000000 --- a/Penumbra/Mods/ModManager.cs +++ /dev/null @@ -1,335 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Dalamud.Logging; -using Newtonsoft.Json.Linq; -using Penumbra.GameData.ByteString; -using Penumbra.Meta; -using Penumbra.Util; - -namespace Penumbra.Mods; - -public partial class Mod -{ - public enum ChangeType - { - Added, - Removed, - Changed, - } - - // The ModManager handles the basic mods installed to the mod directory. - // It also contains the CollectionManager that handles all collections. - public class Manager : IEnumerable< Mod > - { - public DirectoryInfo BasePath { get; private set; } = null!; - - private readonly List< Mod > _mods = new(); - - public Mod this[ int idx ] - => _mods[ idx ]; - - public IReadOnlyList< Mod > Mods - => _mods; - - public IEnumerator< Mod > GetEnumerator() - => _mods.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - public ModFolder StructuredMods { get; } = ModFileSystem.Root; - - public delegate void ModChangeDelegate( ChangeType type, Mod mod ); - - public event ModChangeDelegate? ModChange; - public event Action? ModDiscoveryStarted; - public event Action? ModDiscoveryFinished; - - public bool Valid { get; private set; } - - public int Count - => _mods.Count; - - public Configuration Config - => Penumbra.Config; - - public void DiscoverMods( string newDir ) - { - SetBaseDirectory( newDir, false ); - DiscoverMods(); - } - - private void SetBaseDirectory( string newPath, bool firstTime ) - { - if( !firstTime && string.Equals( newPath, Config.ModDirectory, StringComparison.InvariantCultureIgnoreCase ) ) - { - return; - } - - if( newPath.Length == 0 ) - { - Valid = false; - BasePath = new DirectoryInfo( "." ); - } - else - { - var newDir = new DirectoryInfo( newPath ); - if( !newDir.Exists ) - { - try - { - Directory.CreateDirectory( newDir.FullName ); - newDir.Refresh(); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not create specified mod directory {newDir.FullName}:\n{e}" ); - } - } - - if( !firstTime ) - { - HandleSortOrderFiles( newDir ); - } - - BasePath = newDir; - - Valid = true; - if( Config.ModDirectory != BasePath.FullName ) - { - Config.ModDirectory = BasePath.FullName; - Config.Save(); - } - } - } - - private const string SortOrderFileName = "sort_order.json"; - public static string SortOrderFile = Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), SortOrderFileName ); - - private void HandleSortOrderFiles( DirectoryInfo newDir ) - { - try - { - var mainFile = SortOrderFile; - // Copy old sort order to backup. - var oldSortOrderFile = Path.Combine( BasePath.FullName, SortOrderFileName ); - PluginLog.Debug( "Copying current sort older file to {BackupFile}...", oldSortOrderFile ); - File.Copy( mainFile, oldSortOrderFile, true ); - BasePath = newDir; - var newSortOrderFile = Path.Combine( newDir.FullName, SortOrderFileName ); - // Copy new sort order to main, if it exists. - if( File.Exists( newSortOrderFile ) ) - { - File.Copy( newSortOrderFile, mainFile, true ); - PluginLog.Debug( "Copying stored sort order file from {BackupFile}...", newSortOrderFile ); - } - else - { - File.Delete( mainFile ); - PluginLog.Debug( "Deleting current sort order file...", newSortOrderFile ); - } - } - catch( Exception e ) - { - PluginLog.Error( $"Could not swap Sort Order files:\n{e}" ); - } - } - - public Manager() - { - SetBaseDirectory( Config.ModDirectory, true ); - // TODO - try - { - var data = JObject.Parse( File.ReadAllText( SortOrderFile ) ); - TemporaryModSortOrder = data[ "Data" ]?.ToObject< Dictionary< string, string > >() ?? new Dictionary< string, string >(); - } - catch - { - TemporaryModSortOrder = new Dictionary< string, string >(); - } - } - - public Dictionary< string, string > TemporaryModSortOrder; - - private bool SetSortOrderPath( Mod mod, string path ) - { - mod.Move( path ); - var fixedPath = mod.Order.FullPath; - if( fixedPath.Length == 0 || string.Equals( fixedPath, mod.Meta.Name, StringComparison.InvariantCultureIgnoreCase ) ) - { - Penumbra.ModManager.TemporaryModSortOrder.Remove( mod.BasePath.Name ); - return true; - } - - if( path != fixedPath ) - { - TemporaryModSortOrder[ mod.BasePath.Name ] = fixedPath; - return true; - } - - return false; - } - - private void SetModStructure( bool removeOldPaths = false ) - { - var changes = false; - - foreach( var (folder, path) in TemporaryModSortOrder.ToArray() ) - { - if( path.Length > 0 && _mods.FindFirst( m => m.BasePath.Name == folder, out var mod ) ) - { - changes |= SetSortOrderPath( mod, path ); - } - else if( removeOldPaths ) - { - changes = true; - TemporaryModSortOrder.Remove( folder ); - } - } - - if( changes ) - { - Config.Save(); - } - } - - public void DiscoverMods() - { - ModDiscoveryStarted?.Invoke(); - _mods.Clear(); - BasePath.Refresh(); - - StructuredMods.SubFolders.Clear(); - StructuredMods.Mods.Clear(); - if( Valid && BasePath.Exists ) - { - foreach( var modFolder in BasePath.EnumerateDirectories() ) - { - var mod = LoadMod( StructuredMods, modFolder ); - if( mod == null ) - { - continue; - } - - mod.Index = _mods.Count; - _mods.Add( mod ); - } - - SetModStructure(); - } - - ModDiscoveryFinished?.Invoke(); - } - - public void DeleteMod( DirectoryInfo modFolder ) - { - if( Directory.Exists( modFolder.FullName ) ) - { - try - { - Directory.Delete( modFolder.FullName, true ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not delete the mod {modFolder.Name}:\n{e}" ); - } - } - - var idx = _mods.FindIndex( m => m.BasePath.Name == modFolder.Name ); - if( idx >= 0 ) - { - var mod = _mods[ idx ]; - mod.Order.ParentFolder.RemoveMod( mod ); - _mods.RemoveAt( idx ); - for( var i = idx; i < _mods.Count; ++i ) - { - --_mods[ i ].Index; - } - - ModChange?.Invoke( ChangeType.Removed, mod ); - } - } - - public int AddMod( DirectoryInfo modFolder ) - { - var mod = LoadMod( StructuredMods, modFolder ); - if( mod == null ) - { - return -1; - } - - if( TemporaryModSortOrder.TryGetValue( mod.BasePath.Name, out var sortOrder ) ) - { - if( SetSortOrderPath( mod, sortOrder ) ) - { - Config.Save(); - } - } - - if( _mods.Any( m => m.BasePath.Name == modFolder.Name ) ) - { - return -1; - } - - _mods.Add( mod ); - ModChange?.Invoke( ChangeType.Added, mod ); - - return _mods.Count - 1; - } - - public bool UpdateMod( int idx, bool reloadMeta = false, bool recomputeMeta = false, bool force = false ) - { - var mod = Mods[ idx ]; - var oldName = mod.Meta.Name; - var metaChanges = mod.Meta.RefreshFromFile( mod.MetaFile ) != 0 || force; - var fileChanges = mod.Resources.RefreshModFiles( mod.BasePath ); - - if( !recomputeMeta && !reloadMeta && !metaChanges && fileChanges == 0 ) - { - return false; - } - - if( metaChanges || fileChanges.HasFlag( ResourceChange.Files ) ) - { - mod.ComputeChangedItems(); - if( TemporaryModSortOrder.TryGetValue( mod.BasePath.Name, out var sortOrder ) ) - { - mod.Move( sortOrder ); - var path = mod.Order.FullPath; - if( path != sortOrder ) - { - TemporaryModSortOrder[ mod.BasePath.Name ] = path; - Config.Save(); - } - } - else - { - mod.Order = new SortOrder( StructuredMods, mod.Meta.Name ); - } - } - - var nameChange = !string.Equals( oldName, mod.Meta.Name, StringComparison.InvariantCulture ); - - recomputeMeta |= fileChanges.HasFlag( ResourceChange.Meta ); - if( recomputeMeta ) - { - mod.Resources.MetaManipulations.Update( mod.Resources.MetaFiles, mod.BasePath, mod.Meta ); - mod.Resources.MetaManipulations.SaveToFile( MetaCollection.FileName( mod.BasePath ) ); - } - - // TODO: more specific mod changes? - ModChange?.Invoke( ChangeType.Changed, mod ); - return true; - } - - public bool UpdateMod( Mod mod, bool reloadMeta = false, bool recomputeMeta = false, bool force = false ) - => UpdateMod( Mods.IndexOf( mod ), reloadMeta, recomputeMeta, force ); - - public static FullPath? ResolvePath( Utf8GamePath gameResourcePath ) - => Penumbra.CollectionManager.Default.ResolvePath( gameResourcePath ); - } -} \ No newline at end of file diff --git a/Penumbra/Mods/ModManagerEditExtensions.cs b/Penumbra/Mods/ModManagerEditExtensions.cs deleted file mode 100644 index 82869ad9..00000000 --- a/Penumbra/Mods/ModManagerEditExtensions.cs +++ /dev/null @@ -1,211 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.IO; -using Dalamud.Logging; -using Penumbra.Util; - -namespace Penumbra.Mods; - -// Extracted to keep the main file a bit more clean. -// Contains all change functions on a specific mod that also require corresponding changes to collections. -public static class ModManagerEditExtensions -{ - public static bool RenameMod( this Mod.Manager manager, string newName, Mod mod ) - { - if( newName.Length == 0 || string.Equals( newName, mod.Meta.Name, StringComparison.InvariantCulture ) ) - { - return false; - } - - mod.Meta.Name = newName; - mod.SaveMeta(); - - return true; - } - - public static bool ChangeSortOrder( this Mod.Manager manager, Mod mod, string newSortOrder ) - { - if( string.Equals( mod.Order.FullPath, newSortOrder, StringComparison.InvariantCultureIgnoreCase ) ) - { - return false; - } - - var inRoot = new Mod.SortOrder( manager.StructuredMods, mod.Meta.Name ); - if( newSortOrder == string.Empty || newSortOrder == inRoot.SortOrderName ) - { - mod.Order = inRoot; - manager.TemporaryModSortOrder.Remove( mod.BasePath.Name ); - } - else - { - mod.Move( newSortOrder ); - manager.TemporaryModSortOrder[ mod.BasePath.Name ] = mod.Order.FullPath; - } - - Penumbra.Config.Save(); - - return true; - } - - public static bool RenameModFolder( this Mod.Manager manager, Mod mod, DirectoryInfo newDir, bool move = true ) - { - if( move ) - { - newDir.Refresh(); - if( newDir.Exists ) - { - return false; - } - - var oldDir = new DirectoryInfo( mod.BasePath.FullName ); - try - { - oldDir.MoveTo( newDir.FullName ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error while renaming directory {oldDir.FullName} to {newDir.FullName}:\n{e}" ); - return false; - } - } - - var oldBasePath = mod.BasePath; - mod.BasePath = newDir; - mod.MetaFile = Mod.MetaFileInfo( newDir ); - manager.UpdateMod( mod ); - - if( manager.TemporaryModSortOrder.ContainsKey( oldBasePath.Name ) ) - { - manager.TemporaryModSortOrder[ newDir.Name ] = manager.TemporaryModSortOrder[ oldBasePath.Name ]; - manager.TemporaryModSortOrder.Remove( oldBasePath.Name ); - Penumbra.Config.Save(); - } - - var idx = manager.Mods.IndexOf( mod ); - foreach( var collection in Penumbra.CollectionManager ) - { - if( collection.Settings[ idx ] != null ) - { - collection.Save(); - } - } - - return true; - } - - public static bool ChangeModGroup( this Mod.Manager manager, string oldGroupName, string newGroupName, Mod mod, - SelectType type = SelectType.Single ) - { - if( newGroupName == oldGroupName || mod.Meta.Groups.ContainsKey( newGroupName ) ) - { - return false; - } - - if( mod.Meta.Groups.TryGetValue( oldGroupName, out var oldGroup ) ) - { - if( newGroupName.Length > 0 ) - { - mod.Meta.Groups[ newGroupName ] = new OptionGroup() - { - GroupName = newGroupName, - SelectionType = oldGroup.SelectionType, - Options = oldGroup.Options, - }; - } - - mod.Meta.Groups.Remove( oldGroupName ); - } - else - { - if( newGroupName.Length == 0 ) - { - return false; - } - - mod.Meta.Groups[ newGroupName ] = new OptionGroup() - { - GroupName = newGroupName, - SelectionType = type, - Options = new List< Option >(), - }; - } - - mod.SaveMeta(); - - // TODO to indices - var idx = Penumbra.ModManager.Mods.IndexOf( mod ); - - foreach( var collection in Penumbra.CollectionManager ) - { - var settings = collection.Settings[ idx ]; - if( settings == null ) - { - continue; - } - - if( newGroupName.Length > 0 ) - { - settings.Settings[ newGroupName ] = settings.Settings.TryGetValue( oldGroupName, out var value ) ? value : 0; - } - - settings.Settings.Remove( oldGroupName ); - collection.Save(); - } - - return true; - } - - public static bool RemoveModOption( this Mod.Manager manager, int optionIdx, OptionGroup group, Mod mod ) - { - if( optionIdx < 0 || optionIdx >= group.Options.Count ) - { - return false; - } - - group.Options.RemoveAt( optionIdx ); - mod.SaveMeta(); - - static int MoveMultiSetting( int oldSetting, int idx ) - { - var bitmaskFront = ( 1 << idx ) - 1; - var bitmaskBack = ~( bitmaskFront | ( 1 << idx ) ); - return ( oldSetting & bitmaskFront ) | ( ( oldSetting & bitmaskBack ) >> 1 ); - } - - var idx = Penumbra.ModManager.Mods.IndexOf( mod ); // TODO - foreach( var collection in Penumbra.CollectionManager ) - { - var settings = collection.Settings[ idx ]; - if( settings == null ) - { - continue; - } - - if( !settings.Settings.TryGetValue( group.GroupName, out var setting ) ) - { - setting = 0; - } - - var newSetting = group.SelectionType switch - { - SelectType.Single => setting >= optionIdx ? setting - 1 : setting, - SelectType.Multi => MoveMultiSetting( setting, optionIdx ), - _ => throw new InvalidEnumArgumentException(), - }; - - if( newSetting != setting ) - { - settings.Settings[ group.GroupName ] = newSetting; - collection.Save(); - if( collection.HasCache && settings.Enabled ) - { - collection.CalculateEffectiveFileList( mod.Resources.MetaManipulations.Count > 0, - Penumbra.CollectionManager.Default == collection ); - } - } - } - - return true; - } -} \ No newline at end of file diff --git a/Penumbra/Mods/ModMeta.cs b/Penumbra/Mods/ModMeta.cs deleted file mode 100644 index 253839ce..00000000 --- a/Penumbra/Mods/ModMeta.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Dalamud.Logging; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using OtterGui; -using Penumbra.GameData.ByteString; -using Penumbra.Util; - -namespace Penumbra.Mods; - -// Contains descriptive data about the mod as well as possible settings and fileswaps. -public class ModMeta -{ - public const uint CurrentFileVersion = 1; - - [Flags] - public enum ChangeType : byte - { - Name = 0x01, - Author = 0x02, - Description = 0x04, - Version = 0x08, - Website = 0x10, - Deletion = 0x20, - } - - public uint FileVersion { get; set; } = CurrentFileVersion; - public LowerString Name { get; set; } = "Mod"; - public LowerString Author { get; set; } = LowerString.Empty; - public string Description { get; set; } = string.Empty; - public string Version { get; set; } = string.Empty; - public string Website { get; set; } = string.Empty; - - public bool HasGroupsWithConfig = false; - - public bool RefreshHasGroupsWithConfig() - { - var oldValue = HasGroupsWithConfig; - HasGroupsWithConfig = Groups.Values.Any( g => g.Options.Count > 1 || g.SelectionType == SelectType.Multi && g.Options.Count == 1 ); - return oldValue != HasGroupsWithConfig; - } - - public ChangeType RefreshFromFile( FileInfo filePath ) - { - var newMeta = LoadFromFile( filePath ); - if( newMeta == null ) - { - return ChangeType.Deletion; - } - - ChangeType changes = 0; - - if( Name != newMeta.Name ) - { - changes |= ChangeType.Name; - Name = newMeta.Name; - } - - if( Author != newMeta.Author ) - { - changes |= ChangeType.Author; - Author = newMeta.Author; - } - - if( Description != newMeta.Description ) - { - changes |= ChangeType.Description; - Description = newMeta.Description; - } - - if( Version != newMeta.Version ) - { - changes |= ChangeType.Version; - Version = newMeta.Version; - } - - if( Website != newMeta.Website ) - { - changes |= ChangeType.Website; - Website = newMeta.Website; - } - - return changes; - } - - [JsonProperty( ItemConverterType = typeof( FullPath.FullPathConverter ) )] - public Dictionary< Utf8GamePath, FullPath > FileSwaps { get; set; } = new(); - - public Dictionary< string, OptionGroup > Groups { get; set; } = new(); - - public static ModMeta? LoadFromFile( FileInfo filePath ) - { - try - { - var text = File.ReadAllText( filePath.FullName ); - - var meta = JsonConvert.DeserializeObject< ModMeta >( text, - new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore } ); - if( meta != null ) - { - meta.RefreshHasGroupsWithConfig(); - Migration.Migrate( meta, text ); - } - - return meta; - } - catch( Exception e ) - { - PluginLog.Error( $"Could not load mod meta:\n{e}" ); - return null; - } - } - - - public void SaveToFile( FileInfo filePath ) - { - try - { - var text = JsonConvert.SerializeObject( this, Formatting.Indented ); - File.WriteAllText( filePath.FullName, text ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not write meta file for mod {Name} to {filePath.FullName}:\n{e}" ); - } - } - - private static class Migration - { - public static void Migrate( ModMeta meta, string text ) - { - MigrateV0ToV1( meta, text ); - } - - private static void MigrateV0ToV1( ModMeta meta, string text ) - { - if( meta.FileVersion > 0 ) - { - return; - } - - var data = JObject.Parse( text ); - var swaps = data[ "FileSwaps" ]?.ToObject< Dictionary< Utf8GamePath, FullPath > >() - ?? new Dictionary< Utf8GamePath, FullPath >(); - var groups = data[ "Groups" ]?.ToObject< Dictionary< string, OptionGroupV0 > >() ?? new Dictionary< string, OptionGroupV0 >(); - foreach( var group in groups.Values ) - { } - - foreach( var swap in swaps ) - { } - - //var meta = - } - - - private struct OptionV0 - { - public string OptionName = string.Empty; - public string OptionDesc = string.Empty; - - [JsonProperty( ItemConverterType = typeof( SingleOrArrayConverter< Utf8GamePath > ) )] - public Dictionary< Utf8RelPath, HashSet< Utf8GamePath > > OptionFiles = new(); - - public OptionV0() - { } - } - - private struct OptionGroupV0 - { - public string GroupName = string.Empty; - - [JsonConverter( typeof( Newtonsoft.Json.Converters.StringEnumConverter ) )] - public SelectType SelectionType = SelectType.Single; - - public List< OptionV0 > Options = new(); - - public OptionGroupV0() - { } - } - } -} \ No newline at end of file diff --git a/Penumbra/Mods/ModResources.cs b/Penumbra/Mods/ModResources.cs deleted file mode 100644 index cd05fc7e..00000000 --- a/Penumbra/Mods/ModResources.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Penumbra.GameData.ByteString; -using Penumbra.Meta; - -namespace Penumbra.Mods; - -[Flags] -public enum ResourceChange -{ - None = 0, - Files = 1, - Meta = 2, -} - -// Contains static mod data that should only change on filesystem changes. -public class ModResources -{ - public List< FullPath > ModFiles { get; private set; } = new(); - public List< FullPath > MetaFiles { get; private set; } = new(); - - public MetaCollection MetaManipulations { get; private set; } = new(); - - - private void ForceManipulationsUpdate( ModMeta meta, DirectoryInfo basePath ) - { - MetaManipulations.Update( MetaFiles, basePath, meta ); - MetaManipulations.SaveToFile( MetaCollection.FileName( basePath ) ); - } - - public void SetManipulations( ModMeta meta, DirectoryInfo basePath, bool validate = true ) - { - var newManipulations = MetaCollection.LoadFromFile( MetaCollection.FileName( basePath ) ); - if( newManipulations == null ) - { - ForceManipulationsUpdate( meta, basePath ); - } - else - { - MetaManipulations = newManipulations; - if( validate && !MetaManipulations.Validate( meta ) ) - { - ForceManipulationsUpdate( meta, basePath ); - } - } - } - - // Update the current set of files used by the mod, - // returns true if anything changed. - public ResourceChange RefreshModFiles( DirectoryInfo basePath ) - { - List< FullPath > tmpFiles = new(ModFiles.Count); - List< FullPath > tmpMetas = new(MetaFiles.Count); - // we don't care about any _files_ in the root dir, but any folders should be a game folder/file combo - foreach( var file in basePath.EnumerateDirectories() - .SelectMany( dir => dir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) - .Select( f => new FullPath( f ) ) - .OrderBy( f => f.FullName ) ) - { - switch( file.Extension.ToLowerInvariant() ) - { - case ".meta": - case ".rgsp": - tmpMetas.Add( file ); - break; - default: - tmpFiles.Add( file ); - break; - } - } - - ResourceChange changes = 0; - if( !tmpFiles.Select( f => f.FullName ).SequenceEqual( ModFiles.Select( f => f.FullName ) ) ) - { - ModFiles = tmpFiles; - changes |= ResourceChange.Files; - } - - if( !tmpMetas.Select( f => f.FullName ).SequenceEqual( MetaFiles.Select( f => f.FullName ) ) ) - { - MetaFiles = tmpMetas; - changes |= ResourceChange.Meta; - } - - return changes; - } -} \ No newline at end of file diff --git a/Penumbra/Mods/NamedModSettings.cs b/Penumbra/Mods/NamedModSettings.cs deleted file mode 100644 index 5a0ded71..00000000 --- a/Penumbra/Mods/NamedModSettings.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -namespace Penumbra.Mods; - -// Contains settings with the option selections stored by names instead of index. -// This is meant to make them possibly more portable when we support importing collections from other users. -// Enabled does not exist, because disabled mods would not be exported in this way. -public class NamedModSettings -{ - public int Priority { get; set; } - public Dictionary< string, HashSet< string > > Settings { get; set; } = new(); - - public void AddFromModSetting( ModSettings s, ModMeta meta ) - { - Priority = s.Priority; - Settings = s.Settings.Keys.ToDictionary( k => k, _ => new HashSet< string >() ); - - foreach( var kvp in Settings ) - { - if( !meta.Groups.TryGetValue( kvp.Key, out var info ) ) - { - continue; - } - - var setting = s.Settings[ kvp.Key ]; - if( info.SelectionType == SelectType.Single ) - { - var name = setting < info.Options.Count - ? info.Options[ setting ].OptionName - : info.Options[ 0 ].OptionName; - kvp.Value.Add( name ); - } - else - { - for( var i = 0; i < info.Options.Count; ++i ) - { - if( ( ( setting >> i ) & 1 ) != 0 ) - { - kvp.Value.Add( info.Options[ i ].OptionName ); - } - } - } - } - } -} \ No newline at end of file diff --git a/Penumbra/Mods/Subclasses/Mod2.Files.SubMod.cs b/Penumbra/Mods/Subclasses/Mod2.Files.SubMod.cs index 766c59da..ccbf2df8 100644 --- a/Penumbra/Mods/Subclasses/Mod2.Files.SubMod.cs +++ b/Penumbra/Mods/Subclasses/Mod2.Files.SubMod.cs @@ -6,6 +6,7 @@ using Dalamud.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Penumbra.GameData.ByteString; +using Penumbra.Importer; using Penumbra.Meta.Manipulations; namespace Penumbra.Mods; @@ -113,5 +114,82 @@ public partial class Mod2 } } } + + public void IncorporateMetaChanges( DirectoryInfo basePath, bool delete ) + { + foreach( var (key, file) in Files.ToList() ) + { + try + { + switch( file.Extension ) + { + case ".meta": + FileData.Remove( key ); + if( !file.Exists ) + { + continue; + } + + var meta = new TexToolsMeta( File.ReadAllBytes( file.FullName ) ); + if( delete ) + { + File.Delete( file.FullName ); + } + + foreach( var manip in meta.EqpManipulations ) + { + ManipulationData.Add( manip ); + } + + foreach( var manip in meta.EqdpManipulations ) + { + ManipulationData.Add( manip ); + } + + foreach( var manip in meta.EstManipulations ) + { + ManipulationData.Add( manip ); + } + + foreach( var manip in meta.GmpManipulations ) + { + ManipulationData.Add( manip ); + } + + foreach( var manip in meta.ImcManipulations ) + { + ManipulationData.Add( manip ); + } + + break; + case ".rgsp": + FileData.Remove( key ); + if( !file.Exists ) + { + continue; + } + + var rgsp = TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ) ); + if( delete ) + { + File.Delete( file.FullName ); + } + + foreach( var manip in rgsp.RspManipulations ) + { + ManipulationData.Add( manip ); + } + + break; + default: continue; + } + } + catch( Exception e ) + { + PluginLog.Error( $"Could not incorporate meta changes in mod {basePath} from file {file.FullName}:\n{e}" ); + continue; + } + } + } } } \ No newline at end of file diff --git a/Penumbra/Mods/Subclasses/ModSettings.cs b/Penumbra/Mods/Subclasses/ModSettings.cs index d32c6d70..d40f578b 100644 --- a/Penumbra/Mods/Subclasses/ModSettings.cs +++ b/Penumbra/Mods/Subclasses/ModSettings.cs @@ -4,72 +4,140 @@ using System.Linq; namespace Penumbra.Mods; + // Contains the settings for a given mod. -public class ModSettings +public class ModSettings2 { - public static readonly ModSettings Empty = new(); - - public bool Enabled { get; set; } + public static readonly ModSettings2 Empty = new(); + public List< uint > Settings { get; init; } = new(); public int Priority { get; set; } - public Dictionary< string, int > Settings { get; set; } = new(); + public bool Enabled { get; set; } - // For backwards compatibility - private Dictionary< string, int > Conf - { - set => Settings = value; - } - - public ModSettings DeepCopy() - { - var settings = new ModSettings + public ModSettings2 DeepCopy() + => new() { Enabled = Enabled, Priority = Priority, - Settings = Settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ), + Settings = Settings.ToList(), }; - return settings; - } - public static ModSettings DefaultSettings( ModMeta meta ) - { - return new ModSettings + public static ModSettings2 DefaultSettings( Mod2 mod ) + => new() { Enabled = false, Priority = 0, - Settings = meta.Groups.ToDictionary( kvp => kvp.Key, _ => 0 ), + Settings = Enumerable.Repeat( 0u, mod.Groups.Count ).ToList(), + }; + + + + public void HandleChanges( ModOptionChangeType type, Mod2 mod, int groupIdx, int optionIdx ) + { + switch( type ) + { + case ModOptionChangeType.GroupAdded: + Settings.Insert( groupIdx, 0 ); + break; + case ModOptionChangeType.GroupDeleted: + Settings.RemoveAt( groupIdx ); + break; + case ModOptionChangeType.OptionDeleted: + var group = mod.Groups[ groupIdx ]; + var config = Settings[ groupIdx ]; + Settings[ groupIdx ] = group.Type switch + { + SelectType.Single => config >= optionIdx ? Math.Max( 0, config - 1 ) : config, + SelectType.Multi => RemoveBit( config, optionIdx ), + _ => config, + }; + break; + } + } + + public void SetValue( Mod2 mod, int groupIdx, uint newValue ) + { + AddMissingSettings( groupIdx + 1 ); + var group = mod.Groups[ groupIdx ]; + Settings[ groupIdx ] = group.Type switch + { + SelectType.Single => ( uint )Math.Max( newValue, group.Count ), + SelectType.Multi => ( ( 1u << group.Count ) - 1 ) & newValue, + _ => newValue, }; } - public bool FixSpecificSetting( string name, ModMeta meta ) + private static uint RemoveBit( uint config, int bit ) { - if( !meta.Groups.TryGetValue( name, out var group ) ) - { - return Settings.Remove( name ); - } - - if( Settings.TryGetValue( name, out var oldSetting ) ) - { - Settings[ name ] = group.SelectionType switch - { - SelectType.Single => Math.Min( Math.Max( oldSetting, 0 ), group.Options.Count - 1 ), - SelectType.Multi => Math.Min( Math.Max( oldSetting, 0 ), ( 1 << group.Options.Count ) - 1 ), - _ => Settings[ group.GroupName ], - }; - return oldSetting != Settings[ group.GroupName ]; - } - - Settings[ name ] = 0; - return true; + var lowMask = ( 1u << bit ) - 1u; + var highMask = ~( ( 1u << ( bit + 1 ) ) - 1u ); + var low = config & lowMask; + var high = ( config & highMask ) >> 1; + return low | high; } - public bool FixInvalidSettings( ModMeta meta ) + internal bool AddMissingSettings( int totalCount ) { - if( meta.Groups.Count == 0 ) + if( totalCount <= Settings.Count ) { return false; } - return Settings.Keys.ToArray().Union( meta.Groups.Keys ) - .Aggregate( false, ( current, name ) => current | FixSpecificSetting( name, meta ) ); + Settings.AddRange( Enumerable.Repeat( 0u, totalCount - Settings.Count ) ); + return true; + } + + public struct SavedSettings + { + public Dictionary< string, uint > Settings; + public int Priority; + public bool Enabled; + + public SavedSettings DeepCopy() + => new() + { + Enabled = Enabled, + Priority = Priority, + Settings = Settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ), + }; + + public SavedSettings( ModSettings2 settings, Mod2 mod ) + { + Priority = settings.Priority; + Enabled = settings.Enabled; + Settings = new Dictionary< string, uint >( mod.Groups.Count ); + settings.AddMissingSettings( mod.Groups.Count ); + + foreach( var (group, setting) in mod.Groups.Zip( settings.Settings ) ) + { + Settings.Add( group.Name, setting ); + } + } + + public bool ToSettings( Mod2 mod, out ModSettings2 settings ) + { + var list = new List< uint >( mod.Groups.Count ); + var changes = Settings.Count != mod.Groups.Count; + foreach( var group in mod.Groups ) + { + if( Settings.TryGetValue( group.Name, out var config ) ) + { + list.Add( config ); + } + else + { + list.Add( 0 ); + changes = true; + } + } + + settings = new ModSettings2 + { + Enabled = Enabled, + Priority = Priority, + Settings = list, + }; + + return changes; + } } } \ No newline at end of file diff --git a/Penumbra/Mods/Subclasses/SelectType.cs b/Penumbra/Mods/Subclasses/SelectType.cs new file mode 100644 index 00000000..0843729c --- /dev/null +++ b/Penumbra/Mods/Subclasses/SelectType.cs @@ -0,0 +1,7 @@ +namespace Penumbra.Mods; + +public enum SelectType +{ + Single, + Multi, +} \ No newline at end of file diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 37ae5260..9bdb5bd4 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Dalamud.Game.Command; +using Dalamud.Interface.Windowing; using Dalamud.Logging; using Dalamud.Plugin; using EmbedIO; @@ -36,20 +37,22 @@ public class Penumbra : IDalamudPlugin public static ResidentResourceManager ResidentResources { get; private set; } = null!; public static CharacterUtility CharacterUtility { get; private set; } = null!; public static MetaFileManager MetaFileManager { get; private set; } = null!; - - public static Mod.Manager ModManager { get; private set; } = null!; + public static Mod2.Manager ModManager { get; private set; } = null!; public static ModCollection.Manager CollectionManager { get; private set; } = null!; + public static SimpleRedirectManager Redirects { get; private set; } = null!; + public static ResourceLoader ResourceLoader { get; private set; } = null!; - public static ResourceLoader ResourceLoader { get; set; } = null!; - public ResourceLogger ResourceLogger { get; } - public PathResolver PathResolver { get; } - public SettingsInterface SettingsInterface { get; } - public MusicManager MusicManager { get; } - public ObjectReloader ObjectReloader { get; } - - public PenumbraApi Api { get; } - public PenumbraIpc Ipc { get; } + public readonly ResourceLogger ResourceLogger; + public readonly PathResolver PathResolver; + public readonly MusicManager MusicManager; + public readonly ObjectReloader ObjectReloader; + public readonly ModFileSystemA ModFileSystem; + public readonly PenumbraApi Api; + public readonly PenumbraIpc Ipc; + private readonly ConfigWindow _configWindow; + private readonly LaunchButton _launchButton; + private readonly WindowSystem _windowSystem; private WebServer? _webServer; @@ -68,12 +71,14 @@ public class Penumbra : IDalamudPlugin ResidentResources = new ResidentResourceManager(); CharacterUtility = new CharacterUtility(); + Redirects = new SimpleRedirectManager(); MetaFileManager = new MetaFileManager(); ResourceLoader = new ResourceLoader( this ); ResourceLogger = new ResourceLogger( ResourceLoader ); - ModManager = new Mod.Manager(); + ModManager = new Mod2.Manager( Config.ModDirectory ); ModManager.DiscoverMods(); CollectionManager = new ModCollection.Manager( ModManager ); + ModFileSystem = ModFileSystemA.Load(); ObjectReloader = new ObjectReloader(); PathResolver = new PathResolver( ResourceLoader ); @@ -92,8 +97,7 @@ public class Penumbra : IDalamudPlugin Api = new PenumbraApi( this ); Ipc = new PenumbraIpc( pluginInterface, Api ); SubscribeItemLinks(); - - SettingsInterface = new SettingsInterface( this ); + SetupInterface( out _configWindow, out _launchButton, out _windowSystem ); if( Config.EnableHttpApi ) { @@ -129,6 +133,24 @@ public class Penumbra : IDalamudPlugin } } + private void SetupInterface( out ConfigWindow cfg, out LaunchButton btn, out WindowSystem system ) + { + cfg = new ConfigWindow( this ); + btn = new LaunchButton( _configWindow ); + system = new WindowSystem( Name ); + system.AddWindow( _configWindow ); + Dalamud.PluginInterface.UiBuilder.Draw += system.Draw; + Dalamud.PluginInterface.UiBuilder.OpenConfigUi += cfg.Toggle; + } + + private void DisposeInterface() + { + Dalamud.PluginInterface.UiBuilder.Draw -= _windowSystem.Draw; + Dalamud.PluginInterface.UiBuilder.OpenConfigUi -= _configWindow.Toggle; + _launchButton.Dispose(); + _configWindow.Dispose(); + } + public bool Enable() { if( Config.EnableMods ) @@ -209,10 +231,11 @@ public class Penumbra : IDalamudPlugin public void Dispose() { + DisposeInterface(); Ipc.Dispose(); Api.Dispose(); - SettingsInterface.Dispose(); ObjectReloader.Dispose(); + ModFileSystem.Dispose(); CollectionManager.Dispose(); Dalamud.Commands.RemoveHandler( CommandName ); @@ -251,7 +274,6 @@ public class Penumbra : IDalamudPlugin CollectionManager.SetCollection( collection, ModCollection.Type.Default ); Dalamud.Chat.Print( $"Set {collection.Name} as default collection." ); - SettingsInterface.ResetDefaultCollection(); return true; default: Dalamud.Chat.Print( @@ -293,7 +315,7 @@ public class Penumbra : IDalamudPlugin } case "debug": { - SettingsInterface.MakeDebugTabVisible(); + // TODO break; } case "enable": @@ -341,7 +363,7 @@ public class Penumbra : IDalamudPlugin return; } - SettingsInterface.FlipVisibility(); + _configWindow.Toggle(); } // Collect all relevant files for penumbra configuration. @@ -349,7 +371,7 @@ public class Penumbra : IDalamudPlugin { var list = new DirectoryInfo( ModCollection.CollectionDirectory ).EnumerateFiles( "*.json" ).ToList(); list.Add( Dalamud.PluginInterface.ConfigFile ); - list.Add( new FileInfo( Mod.Manager.SortOrderFile ) ); + list.Add( new FileInfo( Mod2.Manager.ModFileSystemFile ) ); list.Add( new FileInfo( ModCollection.Manager.ActiveCollectionFile ) ); return list; } diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index a8a7591b..707b9f9e 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -30,6 +30,12 @@ $(MSBuildWarningsAsMessages);MSB3277 + + + + + + PreserveNewest diff --git a/Penumbra/UI/Classes/Colors.cs b/Penumbra/UI/Classes/Colors.cs index 9c95a9bc..f51c99fb 100644 --- a/Penumbra/UI/Classes/Colors.cs +++ b/Penumbra/UI/Classes/Colors.cs @@ -19,6 +19,8 @@ public enum ColorId public static class Colors { + public const uint PressEnterWarningBg = 0xFF202080; + public static (uint DefaultColor, string Name, string Description) Data( this ColorId color ) => color switch { diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs index 13c797d2..6caf5bd8 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs @@ -20,11 +20,11 @@ public partial class ModFileSystemSelector public uint Color; } - private const StringComparison IgnoreCase = StringComparison.InvariantCultureIgnoreCase; - private readonly IReadOnlySet< Mod > _newMods = new HashSet< Mod >(); - private LowerString _modFilter = LowerString.Empty; - private int _filterType = -1; - private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods; + private const StringComparison IgnoreCase = StringComparison.InvariantCultureIgnoreCase; + private readonly IReadOnlySet< Mod2 > _newMods = new HashSet< Mod2 >(); + private LowerString _modFilter = LowerString.Empty; + private int _filterType = -1; + private ModFilter _stateFilter = ModFilterExtensions.UnfilteredStateMods; private void SetFilterTooltip() { @@ -75,7 +75,7 @@ public partial class ModFileSystemSelector // Folders have default state and are filtered out on the direct string instead of the other options. // If any filter is set, they should be hidden by default unless their children are visible, // or they contain the path search string. - protected override bool ApplyFiltersAndState( FileSystem< Mod >.IPath path, out ModState state ) + protected override bool ApplyFiltersAndState( FileSystem< Mod2 >.IPath path, out ModState state ) { if( path is ModFileSystemA.Folder f ) { @@ -88,21 +88,21 @@ public partial class ModFileSystemSelector } // Apply the string filters. - private bool ApplyStringFilters( ModFileSystemA.Leaf leaf, Mod mod ) + private bool ApplyStringFilters( ModFileSystemA.Leaf leaf, Mod2 mod ) { return _filterType switch { -1 => false, - 0 => !( leaf.FullName().Contains( _modFilter.Lower, IgnoreCase ) || mod.Meta.Name.Contains( _modFilter ) ), - 1 => !mod.Meta.Name.Contains( _modFilter ), - 2 => !mod.Meta.Author.Contains( _modFilter ), + 0 => !( leaf.FullName().Contains( _modFilter.Lower, IgnoreCase ) || mod.Name.Contains( _modFilter ) ), + 1 => !mod.Name.Contains( _modFilter ), + 2 => !mod.Author.Contains( _modFilter ), 3 => !mod.LowerChangedItemsString.Contains( _modFilter ), _ => false, // Should never happen }; } // Only get the text color for a mod if no filters are set. - private uint GetTextColor( Mod mod, ModSettings? settings, ModCollection collection ) + private uint GetTextColor( Mod2 mod, ModSettings2? settings, ModCollection collection ) { if( _newMods.Contains( mod ) ) { @@ -130,14 +130,14 @@ public partial class ModFileSystemSelector : ColorId.HandledConflictMod.Value(); } - private bool CheckStateFilters( Mod mod, ModSettings? settings, ModCollection collection, ref ModState state ) + private bool CheckStateFilters( Mod2 mod, ModSettings2? settings, ModCollection collection, ref ModState state ) { var isNew = _newMods.Contains( mod ); // Handle mod details. - if( CheckFlags( mod.Resources.ModFiles.Count, ModFilter.HasNoFiles, ModFilter.HasFiles ) - || CheckFlags( mod.Meta.FileSwaps.Count, ModFilter.HasNoFileSwaps, ModFilter.HasFileSwaps ) - || CheckFlags( mod.Resources.MetaManipulations.Count, ModFilter.HasNoMetaManipulations, ModFilter.HasMetaManipulations ) - || CheckFlags( mod.Meta.HasGroupsWithConfig ? 1 : 0, ModFilter.HasNoConfig, ModFilter.HasConfig ) + if( CheckFlags( mod.TotalFileCount, ModFilter.HasNoFiles, ModFilter.HasFiles ) + || CheckFlags( mod.TotalSwapCount, ModFilter.HasNoFileSwaps, ModFilter.HasFileSwaps ) + || CheckFlags( mod.TotalManipulations, ModFilter.HasNoMetaManipulations, ModFilter.HasMetaManipulations ) + || CheckFlags( mod.HasOptions ? 1 : 0, ModFilter.HasNoConfig, ModFilter.HasConfig ) || CheckFlags( isNew ? 1 : 0, ModFilter.NotNew, ModFilter.IsNew ) ) { return true; diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.cs b/Penumbra/UI/Classes/ModFileSystemSelector.cs index a34b3bd6..57ec835f 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.cs @@ -12,12 +12,12 @@ using Penumbra.Mods; namespace Penumbra.UI.Classes; -public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, ModFileSystemSelector.ModState > +public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod2, ModFileSystemSelector.ModState > { - public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty; + public ModSettings2 SelectedSettings { get; private set; } = ModSettings2.Empty; public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty; - public ModFileSystemSelector( ModFileSystemA fileSystem, IReadOnlySet newMods ) + public ModFileSystemSelector( ModFileSystemA fileSystem, IReadOnlySet< Mod2 > newMods ) : base( fileSystem ) { _newMods = newMods; @@ -29,13 +29,19 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod AddButton( DeleteModButton, 1000 ); SetFilterTooltip(); - Penumbra.CollectionManager.CollectionChanged += OnCollectionChange; + Penumbra.CollectionManager.CollectionChanged += OnCollectionChange; + Penumbra.CollectionManager.Current.ModSettingChanged += OnSettingChange; + Penumbra.CollectionManager.Current.InheritanceChanged += OnInheritanceChange; + Penumbra.ModManager.ModDiscoveryStarted += StoreCurrentSelection; + Penumbra.ModManager.ModDiscoveryFinished += RestoreLastSelection; OnCollectionChange( ModCollection.Type.Current, null, Penumbra.CollectionManager.Current, null ); } public override void Dispose() { base.Dispose(); + Penumbra.ModManager.ModDiscoveryStarted -= StoreCurrentSelection; + Penumbra.ModManager.ModDiscoveryFinished -= RestoreLastSelection; Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange; Penumbra.CollectionManager.Current.InheritanceChanged -= OnInheritanceChange; Penumbra.CollectionManager.CollectionChanged -= OnCollectionChange; @@ -54,11 +60,11 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod protected override uint FolderLineColor => ColorId.FolderLine.Value(); - protected override void DrawLeafName( FileSystem< Mod >.Leaf leaf, in ModState state, bool selected ) + protected override void DrawLeafName( FileSystem< Mod2 >.Leaf leaf, in ModState state, bool selected ) { var flags = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags; using var c = ImRaii.PushColor( ImGuiCol.Text, state.Color ); - using var _ = ImRaii.TreeNode( leaf.Value.Meta.Name, flags ); + using var _ = ImRaii.TreeNode( leaf.Value.Name, flags ); } @@ -126,7 +132,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod } // Automatic cache update functions. - private void OnSettingChange( ModSettingChange type, int modIdx, int oldValue, string? optionName, bool inherited ) + private void OnSettingChange( ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool inherited ) { // TODO: maybe make more efficient SetFilterDirty(); @@ -169,13 +175,32 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod { if( newSelection == null ) { - SelectedSettings = ModSettings.Empty; + SelectedSettings = ModSettings2.Empty; SelectedSettingCollection = ModCollection.Empty; } else { ( var settings, SelectedSettingCollection ) = Penumbra.CollectionManager.Current[ newSelection.Value.Index ]; - SelectedSettings = settings ?? ModSettings.Empty; + SelectedSettings = settings ?? ModSettings2.Empty; + } + } + + // Keep selections across rediscoveries if possible. + private string _lastSelectedDirectory = string.Empty; + + private void StoreCurrentSelection() + { + _lastSelectedDirectory = Selected?.BasePath.FullName ?? string.Empty; + ClearSelection(); + } + + private void RestoreLastSelection() + { + if( _lastSelectedDirectory.Length > 0 ) + { + SelectedLeaf = ( ModFileSystemA.Leaf? )FileSystem.Root.GetAllDescendants( SortMode.Lexicographical ) + .FirstOrDefault( l => l is ModFileSystemA.Leaf m && m.Value.BasePath.FullName == _lastSelectedDirectory ); + _lastSelectedDirectory = string.Empty; } } } \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.CollectionsTab.cs b/Penumbra/UI/ConfigWindow.CollectionsTab.cs new file mode 100644 index 00000000..eeacb28e --- /dev/null +++ b/Penumbra/UI/ConfigWindow.CollectionsTab.cs @@ -0,0 +1,22 @@ +using System.Numerics; +using OtterGui.Raii; + +namespace Penumbra.UI; + +public partial class ConfigWindow +{ + public void DrawCollectionsTab() + { + using var tab = ImRaii.TabItem( "Collections" ); + if( !tab ) + { + return; + } + + using var child = ImRaii.Child( "##CollectionsTab", -Vector2.One ); + if( !child ) + { + return; + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.DebugTab.cs b/Penumbra/UI/ConfigWindow.DebugTab.cs new file mode 100644 index 00000000..a76969b0 --- /dev/null +++ b/Penumbra/UI/ConfigWindow.DebugTab.cs @@ -0,0 +1,35 @@ +using System.Numerics; +using OtterGui.Raii; + +namespace Penumbra.UI; + +public partial class ConfigWindow +{ +#if DEBUG + private const bool DefaultVisibility = true; +#else + private const bool DefaultVisibility = false; +#endif + + public bool DebugTabVisible = DefaultVisibility; + + public void DrawDebugTab() + { + if( !DebugTabVisible ) + { + return; + } + + using var tab = ImRaii.TabItem( "Debug" ); + if( !tab ) + { + return; + } + + using var child = ImRaii.Child( "##DebugTab", -Vector2.One ); + if( !child ) + { + return; + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.EffectiveTab.cs b/Penumbra/UI/ConfigWindow.EffectiveTab.cs new file mode 100644 index 00000000..db44fc62 --- /dev/null +++ b/Penumbra/UI/ConfigWindow.EffectiveTab.cs @@ -0,0 +1,27 @@ +using System.Numerics; +using OtterGui.Raii; + +namespace Penumbra.UI; + +public partial class ConfigWindow +{ + public void DrawEffectiveChangesTab() + { + if( !Penumbra.Config.ShowAdvanced ) + { + return; + } + + using var tab = ImRaii.TabItem( "Effective Changes" ); + if( !tab ) + { + return; + } + + using var child = ImRaii.Child( "##EffectiveChangesTab", -Vector2.One ); + if( !child ) + { + return; + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/UiHelpers.cs b/Penumbra/UI/ConfigWindow.Misc.cs similarity index 86% rename from Penumbra/UI/UiHelpers.cs rename to Penumbra/UI/ConfigWindow.Misc.cs index 3d730f57..d4608546 100644 --- a/Penumbra/UI/UiHelpers.cs +++ b/Penumbra/UI/ConfigWindow.Misc.cs @@ -2,13 +2,19 @@ using System.Numerics; using ImGuiNET; using Lumina.Data.Parsing; using Lumina.Excel.GeneratedSheets; +using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; using Penumbra.UI.Custom; namespace Penumbra.UI; -public partial class SettingsInterface +public partial class ConfigWindow { + internal static unsafe void Text( Utf8String s ) + { + ImGuiNative.igTextUnformatted( s.Path, s.Path + s.Length ); + } + internal void DrawChangedItem( string name, object? data, float itemIdOffset = 0 ) { var ret = ImGui.Selectable( name ) ? MouseButton.Left : MouseButton.None; diff --git a/Penumbra/UI/ConfigWindow.ModsTab.cs b/Penumbra/UI/ConfigWindow.ModsTab.cs new file mode 100644 index 00000000..8b5bc12e --- /dev/null +++ b/Penumbra/UI/ConfigWindow.ModsTab.cs @@ -0,0 +1,22 @@ +using System.Numerics; +using OtterGui.Raii; + +namespace Penumbra.UI; + +public partial class ConfigWindow +{ + public void DrawModsTab() + { + using var tab = ImRaii.TabItem( "Mods" ); + if( !tab ) + { + return; + } + + using var child = ImRaii.Child( "##ModsTab", -Vector2.One ); + if( !child ) + { + return; + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ResourceTab.cs b/Penumbra/UI/ConfigWindow.ResourceTab.cs new file mode 100644 index 00000000..db1e1d67 --- /dev/null +++ b/Penumbra/UI/ConfigWindow.ResourceTab.cs @@ -0,0 +1,27 @@ +using System.Numerics; +using OtterGui.Raii; + +namespace Penumbra.UI; + +public partial class ConfigWindow +{ + public void DrawResourceManagerTab() + { + if( !DebugTabVisible ) + { + return; + } + + using var tab = ImRaii.TabItem( "Resource Manager" ); + if( !tab ) + { + return; + } + + using var child = ImRaii.Child( "##ResourceManagerTab", -Vector2.One ); + if( !child ) + { + return; + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.SettingsTab.cs b/Penumbra/UI/ConfigWindow.SettingsTab.cs new file mode 100644 index 00000000..224adccf --- /dev/null +++ b/Penumbra/UI/ConfigWindow.SettingsTab.cs @@ -0,0 +1,317 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.Components; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.GameData.ByteString; +using Penumbra.UI.Classes; + +namespace Penumbra.UI; + +public partial class ConfigWindow +{ + private string _newModDirectory = string.Empty; + + private static bool DrawPressEnterWarning( string old ) + { + using var color = ImRaii.PushColor( ImGuiCol.Button, Colors.PressEnterWarningBg ); + var w = new Vector2( ImGui.CalcItemWidth(), 0 ); + return ImGui.Button( $"Press Enter or Click Here to Save (Current Directory: {old})", w ); + } + + private static void DrawOpenDirectoryButton( int id, DirectoryInfo directory, bool condition ) + { + using var _ = ImRaii.PushId( id ); + var ret = ImGui.Button( "Open Directory" ); + ImGuiUtil.HoverTooltip( "Open this directory in your configured file explorer." ); + if( ret && condition && Directory.Exists( directory.FullName ) ) + { + Process.Start( new ProcessStartInfo( directory.FullName ) + { + UseShellExecute = true, + } ); + } + } + + private void DrawRootFolder() + { + using var group = ImRaii.Group(); + ImGui.SetNextItemWidth( _inputTextWidth.X ); + var save = ImGui.InputText( "Root Directory", ref _newModDirectory, 255, ImGuiInputTextFlags.EnterReturnsTrue ); + ImGui.SameLine(); + ImGuiComponents.HelpMarker( "This is where Penumbra will store your extracted mod files.\n" + + "TTMP files are not copied, just extracted.\n" + + "This directory needs to be accessible and you need write access here.\n" + + "It is recommended that this directory is placed on a fast hard drive, preferably an SSD.\n" + + "It should also be placed near the root of a logical drive - the shorter the total path to this folder, the better.\n" + + "Definitely do not place it in your Dalamud directory or any sub-directory thereof." ); + ImGui.SameLine(); + DrawOpenDirectoryButton( 0, Penumbra.ModManager.BasePath, Penumbra.ModManager.Valid ); + group.Dispose(); + + if( Penumbra.Config.ModDirectory == _newModDirectory || _newModDirectory.Length == 0 ) + { + return; + } + + if( save || DrawPressEnterWarning( Penumbra.Config.ModDirectory ) ) + { + Penumbra.ModManager.DiscoverMods( _newModDirectory ); + } + } + + + private void DrawRediscoverButton() + { + if( ImGui.Button( "Rediscover Mods" ) ) + { + Penumbra.ModManager.DiscoverMods(); + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( "Force Penumbra to completely re-scan your root directory as if it was restarted." ); + } + + private void DrawEnabledBox() + { + var enabled = Penumbra.Config.EnableMods; + if( ImGui.Checkbox( "Enable Mods", ref enabled ) ) + { + _penumbra.SetEnabled( enabled ); + } + } + + private void DrawShowAdvancedBox() + { + var showAdvanced = Penumbra.Config.ShowAdvanced; + if( ImGui.Checkbox( "Show Advanced Settings", ref showAdvanced ) ) + { + Penumbra.Config.ShowAdvanced = showAdvanced; + Penumbra.Config.Save(); + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( "Enable some advanced options in this window and in the mod selector.\n" + + "This is required to enable manually editing any mod information." ); + } + + private void DrawFolderSortType() + { + // TODO provide all options + var foldersFirst = Penumbra.Config.SortFoldersFirst; + if( ImGui.Checkbox( "Sort Mod-Folders Before Mods", ref foldersFirst ) ) + { + Penumbra.Config.SortFoldersFirst = foldersFirst; + Selector.SetFilterDirty(); + Penumbra.Config.Save(); + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "Prioritizes all mod-folders in the mod-selector in the Installed Mods tab so that folders come before single mods, instead of being sorted completely alphabetically" ); + } + + private void DrawScaleModSelectorBox() + { + // TODO set scale + var scaleModSelector = Penumbra.Config.ScaleModSelector; + if( ImGui.Checkbox( "Scale Mod Selector With Window Size", ref scaleModSelector ) ) + { + Penumbra.Config.ScaleModSelector = scaleModSelector; + Penumbra.Config.Save(); + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "Instead of keeping the mod-selector in the Installed Mods tab a fixed width, this will let it scale with the total size of the Penumbra window." ); + } + + private void DrawDisableSoundStreamingBox() + { + var tmp = Penumbra.Config.DisableSoundStreaming; + if( ImGui.Checkbox( "Disable Audio Streaming", ref tmp ) && tmp != Penumbra.Config.DisableSoundStreaming ) + { + Penumbra.Config.DisableSoundStreaming = tmp; + Penumbra.Config.Save(); + if( tmp ) + { + _penumbra.MusicManager.DisableStreaming(); + } + else + { + _penumbra.MusicManager.EnableStreaming(); + } + + Penumbra.ModManager.DiscoverMods(); + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "Disable streaming in the games audio engine.\n" + + "If you do not disable streaming, you can not replace sound files in the game (*.scd files), they will be ignored by Penumbra.\n\n" + + "Only touch this if you experience sound problems.\n" + + "If you toggle this, make sure no modified or to-be-modified sound file is currently playing or was recently playing, else you might crash." ); + } + + + private void DrawEnableHttpApiBox() + { + var http = Penumbra.Config.EnableHttpApi; + if( ImGui.Checkbox( "Enable HTTP API", ref http ) ) + { + if( http ) + { + _penumbra.CreateWebServer(); + } + else + { + _penumbra.ShutdownWebServer(); + } + + Penumbra.Config.EnableHttpApi = http; + Penumbra.Config.Save(); + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "Enables other applications, e.g. Anamnesis, to use some Penumbra functions, like requesting redraws." ); + } + + private static void DrawReloadResourceButton() + { + if( ImGui.Button( "Reload Resident Resources" ) ) + { + Penumbra.ResidentResources.Reload(); + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( "Reload some specific files that the game keeps in memory at all times.\n" + + "You usually should not need to do this." ); + } + + private void DrawEnableFullResourceLoggingBox() + { + var tmp = Penumbra.Config.EnableFullResourceLogging; + if( ImGui.Checkbox( "Enable Full Resource Logging", ref tmp ) && tmp != Penumbra.Config.EnableFullResourceLogging ) + { + if( tmp ) + { + Penumbra.ResourceLoader.EnableFullLogging(); + } + else + { + Penumbra.ResourceLoader.DisableFullLogging(); + } + + Penumbra.Config.EnableFullResourceLogging = tmp; + Penumbra.Config.Save(); + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( "[DEBUG] Enable the logging of all ResourceLoader events indiscriminately." ); + } + + private void DrawEnableDebugModeBox() + { + var tmp = Penumbra.Config.DebugMode; + if( ImGui.Checkbox( "Enable Debug Mode", ref tmp ) && tmp != Penumbra.Config.DebugMode ) + { + if( tmp ) + { + Penumbra.ResourceLoader.EnableDebug(); + } + else + { + Penumbra.ResourceLoader.DisableDebug(); + } + + Penumbra.Config.DebugMode = tmp; + Penumbra.Config.Save(); + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( "[DEBUG] Enable the Debug Tab and Resource Manager Tab as well as some additional data collection." ); + } + + private void DrawRequestedResourceLogging() + { + var tmp = Penumbra.Config.EnableResourceLogging; + if( ImGui.Checkbox( "Enable Requested Resource Logging", ref tmp ) ) + { + _penumbra.ResourceLogger.SetState( tmp ); + } + + ImGui.SameLine(); + ImGuiComponents.HelpMarker( "Log all game paths FFXIV requests to the plugin log.\n" + + "You can filter the logged paths for those containing the entered string or matching the regex, if the entered string compiles to a valid regex.\n" + + "Red boundary indicates invalid regex." ); + ImGui.SameLine(); + var tmpString = Penumbra.Config.ResourceLoggingFilter; + using var color = ImRaii.PushColor( ImGuiCol.Border, 0xFF0000B0, !_penumbra.ResourceLogger.ValidRegex ); + using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale, !_penumbra.ResourceLogger.ValidRegex ); + if( ImGui.InputTextWithHint( "##ResourceLogFilter", "Filter...", ref tmpString, Utf8GamePath.MaxGamePathLength ) ) + { + _penumbra.ResourceLogger.SetFilter( tmpString ); + } + } + + private void DrawAdvancedSettings() + { + DrawRequestedResourceLogging(); + DrawDisableSoundStreamingBox(); + DrawEnableHttpApiBox(); + DrawReloadResourceButton(); + DrawEnableDebugModeBox(); + DrawEnableFullResourceLoggingBox(); + } + + public void DrawSettingsTab() + { + using var tab = ImRaii.TabItem( "Settings" ); + if( !tab ) + { + return; + } + + using var child = ImRaii.Child( "##SettingsTab", -Vector2.One, false ); + if( !child ) + { + return; + } + + DrawRootFolder(); + + DrawRediscoverButton(); + + ImGui.Dummy( _verticalSpace ); + DrawEnabledBox(); + + ImGui.Dummy( _verticalSpace ); + DrawFolderSortType(); + DrawScaleModSelectorBox(); + DrawShowAdvancedBox(); + + if( Penumbra.Config.ShowAdvanced ) + { + DrawAdvancedSettings(); + } + + if( ImGui.CollapsingHeader( "Colors" ) ) + { + foreach( var color in Enum.GetValues< ColorId >() ) + { + var (defaultColor, name, description) = color.Data(); + var currentColor = Penumbra.Config.Colors.TryGetValue( color, out var current ) ? current : defaultColor; + if( ImGuiUtil.ColorPicker( name, description, currentColor, c => Penumbra.Config.Colors[ color ] = c, defaultColor ) ) + { + Penumbra.Config.Save(); + } + } + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs new file mode 100644 index 00000000..58250e6b --- /dev/null +++ b/Penumbra/UI/ConfigWindow.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Reflection; +using Dalamud.Interface; +using Dalamud.Interface.Windowing; +using ImGuiNET; +using OtterGui.Raii; +using Penumbra.Mods; +using Penumbra.UI.Classes; + +namespace Penumbra.UI; + +public sealed partial class ConfigWindow : Window, IDisposable +{ + private readonly Penumbra _penumbra; + public readonly ModFileSystemSelector Selector; + + public ConfigWindow( Penumbra penumbra ) + : base( GetLabel() ) + { + _penumbra = penumbra; + Selector = new ModFileSystemSelector( _penumbra.ModFileSystem, new HashSet< Mod2 >() ); // TODO + Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = true; + Dalamud.PluginInterface.UiBuilder.DisableCutsceneUiHide = true; + Dalamud.PluginInterface.UiBuilder.DisableUserUiHide = true; + RespectCloseHotkey = true; + SizeConstraints = new WindowSizeConstraints() + { + MinimumSize = new Vector2( 1024, 768 ), + MaximumSize = new Vector2( 4096, 2160 ), + }; + } + + public override void Draw() + { + using var bar = ImRaii.TabBar( string.Empty, ImGuiTabBarFlags.NoTooltip ); + SetupSizes(); + DrawSettingsTab(); + DrawModsTab(); + DrawCollectionsTab(); + DrawEffectiveChangesTab(); + DrawDebugTab(); + DrawResourceManagerTab(); + } + + public void Dispose() + { + Selector.Dispose(); + } + + private static string GetLabel() + { + var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? ""; + return version.Length == 0 + ? "Penumbra###PenumbraConfigWindow" + : $"Penumbra v{version}###PenumbraConfigWindow"; + } + + private Vector2 _verticalSpace; + private Vector2 _inputTextWidth; + + private void SetupSizes() + { + _verticalSpace = new Vector2( 0, 20f * ImGuiHelpers.GlobalScale ); + _inputTextWidth = new Vector2( 450f * ImGuiHelpers.GlobalScale, 0 ); + } +} \ No newline at end of file diff --git a/Penumbra/UI/LaunchButton.cs b/Penumbra/UI/LaunchButton.cs index f78c9e89..0033916f 100644 --- a/Penumbra/UI/LaunchButton.cs +++ b/Penumbra/UI/LaunchButton.cs @@ -5,38 +5,33 @@ using ImGuiScene; namespace Penumbra.UI; -public partial class SettingsInterface +public class LaunchButton : IDisposable { - private class ManageModsButton : IDisposable + private readonly ConfigWindow _configWindow; + private readonly TextureWrap? _icon; + private readonly TitleScreenMenu.TitleScreenMenuEntry? _entry; + + public LaunchButton( ConfigWindow ui ) { - private readonly SettingsInterface _base; - private readonly TextureWrap? _icon; - private readonly TitleScreenMenu.TitleScreenMenuEntry? _entry; + _configWindow = ui; - public ManageModsButton( SettingsInterface ui ) + _icon = Dalamud.PluginInterface.UiBuilder.LoadImage( Path.Combine( Dalamud.PluginInterface.AssemblyLocation.DirectoryName!, + "tsmLogo.png" ) ); + if( _icon != null ) { - _base = ui; - - _icon = Dalamud.PluginInterface.UiBuilder.LoadImage( Path.Combine( Dalamud.PluginInterface.AssemblyLocation.DirectoryName!, - "tsmLogo.png" ) ); - if( _icon != null ) - { - _entry = Dalamud.TitleScreenMenu.AddEntry( "Manage Penumbra", _icon, OnTriggered ); - } + _entry = Dalamud.TitleScreenMenu.AddEntry( "Manage Penumbra", _icon, OnTriggered ); } + } - private void OnTriggered() - { - _base.FlipVisibility(); - } + private void OnTriggered() + => _configWindow.Toggle(); - public void Dispose() + public void Dispose() + { + _icon?.Dispose(); + if( _entry != null ) { - _icon?.Dispose(); - if( _entry != null ) - { - Dalamud.TitleScreenMenu.RemoveEntry( _entry ); - } + Dalamud.TitleScreenMenu.RemoveEntry( _entry ); } } } \ No newline at end of file diff --git a/Penumbra/UI/SettingsInterface.cs b/Penumbra/UI/SettingsInterface.cs deleted file mode 100644 index b406da71..00000000 --- a/Penumbra/UI/SettingsInterface.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.Numerics; - -namespace Penumbra.UI; - -public partial class SettingsInterface : IDisposable -{ - private const float DefaultVerticalSpace = 20f; - - private static readonly Vector2 AutoFillSize = new(-1, -1); - private static readonly Vector2 ZeroVector = new(0, 0); - - private readonly Penumbra _penumbra; - - private readonly ManageModsButton _manageModsButton; - private readonly SettingsMenu _menu; - - public SettingsInterface( Penumbra penumbra ) - { - _penumbra = penumbra; - _manageModsButton = new ManageModsButton( this ); - _menu = new SettingsMenu( this ); - - Dalamud.PluginInterface.UiBuilder.DisableGposeUiHide = true; - Dalamud.PluginInterface.UiBuilder.Draw += Draw; - Dalamud.PluginInterface.UiBuilder.OpenConfigUi += OpenConfig; - } - - public void Dispose() - { - _manageModsButton.Dispose(); - _menu.InstalledTab.Selector.Cache.Dispose(); - Dalamud.PluginInterface.UiBuilder.Draw -= Draw; - Dalamud.PluginInterface.UiBuilder.OpenConfigUi -= OpenConfig; - } - - private void OpenConfig() - => _menu.Visible = true; - - public void FlipVisibility() - => _menu.Visible = !_menu.Visible; - - public void MakeDebugTabVisible() - => _menu.DebugTabVisible = true; - - public void Draw() - { - _menu.Draw(); - } - - private void ReloadMods() - { - _menu.InstalledTab.Selector.ClearSelection(); - Penumbra.ModManager.DiscoverMods( Penumbra.Config.ModDirectory ); - _menu.InstalledTab.Selector.Cache.TriggerListReset(); - } - - public void ResetDefaultCollection() - => _menu.CollectionsTab.UpdateDefaultIndex(); -} \ No newline at end of file diff --git a/Penumbra/UI/SettingsMenu.cs b/Penumbra/UI/SettingsMenu.cs deleted file mode 100644 index cee41088..00000000 --- a/Penumbra/UI/SettingsMenu.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System.Numerics; -using Dalamud.Interface; -using ImGuiNET; -using Penumbra.UI.Custom; - -namespace Penumbra.UI; - -public partial class SettingsInterface -{ - private class SettingsMenu - { - public static float InputTextWidth - => 450 * ImGuiHelpers.GlobalScale; - - private const string PenumbraSettingsLabel = "PenumbraSettings"; - - public static readonly Vector2 MinSettingsSize = new(800, 450); - public static readonly Vector2 MaxSettingsSize = new(69420, 42069); - - private readonly SettingsInterface _base; - private readonly TabSettings _settingsTab; - private readonly TabImport _importTab; - private readonly TabBrowser _browserTab; - private readonly TabEffective _effectiveTab; - private readonly TabChangedItems _changedItems; - internal readonly TabCollections CollectionsTab; - internal readonly TabInstalled InstalledTab; - - public SettingsMenu( SettingsInterface ui ) - { - _base = ui; - _settingsTab = new TabSettings( _base ); - _importTab = new TabImport( _base ); - _browserTab = new TabBrowser(); - InstalledTab = new TabInstalled( _base, _importTab.NewMods ); - CollectionsTab = new TabCollections( InstalledTab.Selector ); - _effectiveTab = new TabEffective(); - _changedItems = new TabChangedItems( _base ); - } - -#if DEBUG - private const bool DefaultVisibility = true; -#else - private const bool DefaultVisibility = false; -#endif - public bool Visible = DefaultVisibility; - public bool DebugTabVisible = DefaultVisibility; - - public void Draw() - { - if( !Visible ) - { - return; - } - - ImGui.SetNextWindowSizeConstraints( MinSettingsSize, MaxSettingsSize ); -#if DEBUG - var ret = ImGui.Begin( _base._penumbra.PluginDebugTitleStr, ref Visible ); -#else - var ret = ImGui.Begin( _base._penumbra.Name, ref Visible ); -#endif - using var raii = ImGuiRaii.DeferredEnd( ImGui.End ); - if( !ret ) - { - return; - } - - ImGui.BeginTabBar( PenumbraSettingsLabel ); - raii.Push( ImGui.EndTabBar ); - - _settingsTab.Draw(); - CollectionsTab.Draw(); - _importTab.Draw(); - - if( Penumbra.ModManager.Valid && !_importTab.IsImporting() ) - { - _browserTab.Draw(); - InstalledTab.Draw(); - _changedItems.Draw(); - if( Penumbra.Config.ShowAdvanced ) - { - _effectiveTab.Draw(); - } - } - - if( DebugTabVisible ) - { - _base.DrawDebugTab(); - _base.DrawResourceManagerTab(); - } - } - } -} \ No newline at end of file diff --git a/Penumbra/Util/ModelChanger.cs b/Penumbra/Util/ModelChanger.cs index fd9c4eb3..2ff2b37d 100644 --- a/Penumbra/Util/ModelChanger.cs +++ b/Penumbra/Util/ModelChanger.cs @@ -74,11 +74,11 @@ public static class ModelChanger } } - public static bool ChangeModMaterials( Mods.Mod mod, string from, string to ) + public static bool ChangeModMaterials( Mod2 mod, string from, string to ) { if( ValidStrings( from, to ) ) { - return mod.Resources.ModFiles + return mod.AllFiles .Where( f => f.Extension.Equals( ".mdl", StringComparison.InvariantCultureIgnoreCase ) ) .All( file => ChangeMtrl( file, from, to ) >= 0 ); }